From cacdbd9d5d9b92a835058976bf1e55e19a64dd59 Mon Sep 17 00:00:00 2001 From: Yiming Liu Date: Fri, 18 Sep 2026 23:08:19 +0800 Subject: [PATCH 01/14] feat(perfmodel): select exact MoE kernel source Signed-off-by: Yiming Liu --- crates/core/src/perfmodel/config.rs | 7 +- crates/core/src/perfmodel/engine/runtime.rs | 1 + crates/core/src/perfmodel/engine/spec.rs | 8 +- crates/core/src/perfmodel/fpm/config.rs | 70 +++- crates/core/src/perfmodel/fpm/model.rs | 18 +- crates/core/src/perfmodel/fpm/tests.rs | 1 + crates/core/src/perfmodel/memory.rs | 1 + .../core/src/perfmodel/operators/fpm_sol.rs | 1 + crates/core/src/perfmodel/operators/moe.rs | 183 ++++++--- .../perfmodel/operators/moe_expert_compute.rs | 1 + .../core/src/perfmodel/perf_database/moe.rs | 360 ++++++++++++++++-- crates/core/src/perfmodel/py.rs | 19 + crates/core/src/perfmodel/py_ops.rs | 10 +- crates/core/src/python.rs | 19 + python/aisimulate/src/aisimulate/capacity.py | 6 +- .../src/aisimulate/config/common.py | 3 +- .../src/aisimulate/config/engine.py | 1 + python/aisimulate/src/aisimulate/runner.py | 1 + .../src/aisimulate/sdk/task_v1_compat.py | 3 +- .../aisimulate/src/aisimulate/sdk/task_v2.py | 5 + .../src/aisimulate/sweeper/config.py | 1 + .../src/aisimulate_core/sdk/config.py | 15 + .../aisimulate_core/sdk/config_builders.py | 2 + .../src/aisimulate_core/sdk/engine.py | 3 + .../src/aisimulate_core/sdk/memory.py | 7 + .../aisimulate_core/sdk/models/blocks/moe.py | 3 + .../aisimulate_core/sdk/models/deepseek.py | 2 + .../sdk/models/deepseek_v32.py | 2 + .../aisimulate_core/sdk/models/deepseek_v4.py | 1 + .../sdk/models/deepseek_v41.py | 1 + .../src/aisimulate_core/sdk/models/gemma4.py | 1 + .../src/aisimulate_core/sdk/models/kimi_k3.py | 1 + .../aisimulate_core/sdk/models/nemotron_h.py | 2 + .../src/aisimulate_core/sdk/models/qwen35.py | 2 + .../aisimulate_core/sdk/rust_engine_step.py | 3 + .../aisimulate_core/sdk/speculation/dspark.py | 1 + .../cross_package/test_core_public_api.py | 1 + .../unit/sdk/database/test_attention_lanes.py | 6 +- .../unit/sdk/models/test_deepseek_v41.py | 30 ++ .../test_moe_block_builder_followups.py | 5 + .../unit/sdk/task_v2/test_task_config.py | 76 ++++ .../tests/unit/sdk/task_v2/test_v1_compat.py | 11 + .../tests/unit/sdk/test_compile_engine_mtp.py | 21 + .../sdk/test_moe_kernel_source_consumers.py | 148 +++++++ .../tests/unit/sdk/test_rust_engine_step.py | 20 +- 45 files changed, 992 insertions(+), 91 deletions(-) create mode 100644 python/aisimulate/tests/unit/sdk/test_moe_kernel_source_consumers.py diff --git a/crates/core/src/perfmodel/config.rs b/crates/core/src/perfmodel/config.rs index f15ecf0673..0109ddb364 100644 --- a/crates/core/src/perfmodel/config.rs +++ b/crates/core/src/perfmodel/config.rs @@ -105,7 +105,8 @@ pub const ENGINE_CONFIG_SCHEMA_VERSION: u32 = 1; // - 19 (DeepSeek-V4.1 review): Dsv41AttentionOp gained kv_cache_layout, // separating physical backend KV payload from attention arithmetic precision. // Its appended enum changes positional bincode layout; old JSON defaults only. -pub const ENGINE_SPEC_SCHEMA_VERSION: u32 = 19; +// - 20 (AIC-1781): MoeOp gained exact `moe_kernel_source` lane identity. +pub const ENGINE_SPEC_SCHEMA_VERSION: u32 = 20; /// Static engine identity and setup information carried by an /// [`crate::perfmodel::engine::spec::EngineSpec`]. @@ -149,6 +150,10 @@ pub struct EngineConfig { /// Use the backend-verified bounded DeepSeek-V4.1 decoder execution profile. #[serde(default)] pub decoder_replay: bool, + /// Exact collected MoE compute kernel-source lane. Unlike + /// `moe_backend`, this selects one measured MoE table lane. + #[serde(default)] + pub moe_kernel_source: Option, // KV pub kv_block_size: Option, diff --git a/crates/core/src/perfmodel/engine/runtime.rs b/crates/core/src/perfmodel/engine/runtime.rs index f098957e3d..7c96fb3d04 100644 --- a/crates/core/src/perfmodel/engine/runtime.rs +++ b/crates/core/src/perfmodel/engine/runtime.rs @@ -2303,6 +2303,7 @@ mod tests { forward_model: None, fpm_parquet_path: None, decoder_replay: false, + moe_kernel_source: None, kv_block_size: None, parallel: ParallelMapping { tp_size: 8, diff --git a/crates/core/src/perfmodel/engine/spec.rs b/crates/core/src/perfmodel/engine/spec.rs index 48d20a46fc..110277a59a 100644 --- a/crates/core/src/perfmodel/engine/spec.rs +++ b/crates/core/src/perfmodel/engine/spec.rs @@ -319,6 +319,7 @@ mod tests { workload_distribution: "power_law_1.2".into(), is_gated: true, moe_backend: None, + moe_kernel_source: Some("sglang_flashinfer_trtllm_moe".into()), enable_eplb: false, is_context: false, } @@ -826,6 +827,7 @@ mod tests { forward_model: None, fpm_parquet_path: None, decoder_replay: false, + moe_kernel_source: None, kv_block_size: Some(64), parallel: ParallelMapping { tp_size: 8, @@ -1108,11 +1110,11 @@ mod tests { Err(AicError::UnsupportedSchemaVersion { kind: "EngineSpec", got: 18, - expected: 19 + expected: ENGINE_SPEC_SCHEMA_VERSION }) )); - // A false schema19 stamp cannot silently use the JSON-only default. - bytes[..4].copy_from_slice(&19u32.to_le_bytes()); + // A false current-schema stamp cannot silently use the JSON-only default. + bytes[..4].copy_from_slice(&ENGINE_SPEC_SCHEMA_VERSION.to_le_bytes()); assert!(matches!( EngineSpec::from_bincode(&bytes), Err(AicError::EngineSpec(_)) diff --git a/crates/core/src/perfmodel/fpm/config.rs b/crates/core/src/perfmodel/fpm/config.rs index 25a1c8da48..ae365fea1d 100644 --- a/crates/core/src/perfmodel/fpm/config.rs +++ b/crates/core/src/perfmodel/fpm/config.rs @@ -136,6 +136,10 @@ pub struct ForwardPassPerfModelConfig { pub estimator_config: EstimatorConfig, #[serde(default)] pub attention_backend: Option, + /// Exact collected MoE compute kernel-source lane. This is separate from + /// `moe_backend`, which describes topology/runtime behavior. + #[serde(default)] + pub moe_kernel_source: Option, #[serde(default)] pub moe_backend: Option, #[serde(default)] @@ -185,6 +189,7 @@ impl ForwardPassPerfModelConfig { moe_backend: None, enable_eplb: false, wideep_num_slots: None, + moe_kernel_source: None, enable_shared_layer: None, strict_provenance: false, } @@ -227,6 +232,13 @@ impl ForwardPassPerfModelConfig { { return Err(invalid_config("backend_version cannot be empty")); } + if self + .moe_kernel_source + .as_ref() + .is_some_and(|value| value.trim().is_empty()) + { + return Err(invalid_config("moe_kernel_source cannot be empty")); + } if self.tp == 0 || self.pp == 0 || self.attention_dp == 0 { return Err(invalid_config("tp, pp, and attention_dp must be positive")); } @@ -264,6 +276,7 @@ impl ForwardPassPerfModelConfig { } if self.estimation_mode == EstimationMode::FpmInterpolation && (self.enable_eplb + || self.moe_kernel_source.is_some() || self.wideep_num_slots.is_some() || self .moe_backend @@ -271,7 +284,7 @@ impl ForwardPassPerfModelConfig { .is_some_and(|value| value != "default")) { return Err(invalid_config( - "fpm_interpolation does not support EPLB, slots or moe_backend overrides", + "fpm_interpolation does not support EPLB, slots, moe_backend or moe_kernel_source overrides", )); } if self.nextn > 5 { @@ -373,6 +386,61 @@ mod tests { cfg.validate().unwrap(); } + #[test] + fn moe_kernel_source_round_trips_without_pinning_the_default() { + let default = config(serde_json::json!({})); + assert_eq!(default.moe_kernel_source, None); + + let pinned = config(serde_json::json!({ + "moe_kernel_source": "sglang_flashinfer_trtllm_moe" + })); + let round_trip: ForwardPassPerfModelConfig = + serde_json::from_str(&serde_json::to_string(&pinned).unwrap()).unwrap(); + assert_eq!(round_trip, pinned); + } + + #[test] + fn interpolation_rejects_exact_moe_sources_before_fallback() { + for fallback in ["deny", "allow"] { + let cfg = config(serde_json::json!({ + "estimation_mode": "fpm_interpolation", + "fallback_policy": fallback, + "moe_kernel_source": "source_that_does_not_exist" + })); + let error = crate::ForwardPassPerfModel::best_available(cfg).unwrap_err(); + assert!(error.to_string().contains("moe_kernel_source overrides")); + } + config(serde_json::json!({"estimation_mode": "fpm_interpolation"})) + .validate() + .unwrap(); + config(serde_json::json!({"moe_kernel_source": "sglang_flashinfer_trtllm_moe"})) + .validate() + .unwrap(); + } + + #[test] + fn blank_moe_kernel_source_is_rejected_before_estimator_selection() { + for source in ["", " \t"] { + let error = config(serde_json::json!({"moe_kernel_source": source})) + .validate() + .unwrap_err(); + assert_eq!( + error.to_string(), + "invalid engine config: invalid forward pass perf model config: moe_kernel_source cannot be empty" + ); + } + + for estimation_mode in [EstimationMode::Auto, EstimationMode::FpmRegression] { + let mut cfg = config(serde_json::json!({"moe_kernel_source": " \t"})); + cfg.estimation_mode = estimation_mode; + let error = crate::ForwardPassPerfModel::best_available(cfg).unwrap_err(); + assert_eq!( + error.to_string(), + "invalid engine config: invalid forward pass perf model config: moe_kernel_source cannot be empty" + ); + } + } + #[test] fn prompt_lookup_is_distinct_from_mtp_and_validated_before_selection() { let mut cfg = config(serde_json::json!({ diff --git a/crates/core/src/perfmodel/fpm/model.rs b/crates/core/src/perfmodel/fpm/model.rs index a4dee04a02..ecfbc4ecc1 100644 --- a/crates/core/src/perfmodel/fpm/model.rs +++ b/crates/core/src/perfmodel/fpm/model.rs @@ -705,6 +705,7 @@ fn build_native_candidate( ) -> Result<(Engine, PathBuf), AicError> { if config.estimation_mode == EstimationMode::FpmInterpolation && (config.enable_eplb + || config.moe_kernel_source.is_some() || config.wideep_num_slots.is_some() || config .moe_backend @@ -712,7 +713,7 @@ fn build_native_candidate( .is_some_and(|value| value != "default")) { return Err(AicError::UnsupportedModel( - "FPM interpolation does not support EPLB, slots or moe_backend overrides".into(), + "FPM interpolation does not support EPLB, slots, moe_backend or moe_kernel_source overrides".into(), )); } if config.estimation_mode == EstimationMode::FpmInterpolation && config.nextn != 0 { @@ -1115,6 +1116,21 @@ fn can_fallback_to_regression(err: &AicError) -> bool { mod fallback_errors { use super::*; + #[cfg(feature = "python")] + #[test] + fn interpolation_candidate_does_not_discard_exact_moe_source() { + let mut config = ForwardPassPerfModelConfig::new( + "model", + "system", + crate::BackendKind::Sglang, + crate::ForwardPassWorkerType::Decode, + ); + config.moe_kernel_source = Some("source_that_does_not_exist".into()); + config.estimation_mode = EstimationMode::FpmInterpolation; + let error = build_native_candidate(&config).err().unwrap(); + assert!(error.to_string().contains("moe_kernel_source overrides")); + } + #[test] fn corruption_is_not_a_coverage_gap() { let yaml = serde_yaml::from_str::("broken: [").unwrap_err(); diff --git a/crates/core/src/perfmodel/fpm/tests.rs b/crates/core/src/perfmodel/fpm/tests.rs index 23b740697d..1fe9f60d1a 100644 --- a/crates/core/src/perfmodel/fpm/tests.rs +++ b/crates/core/src/perfmodel/fpm/tests.rs @@ -109,6 +109,7 @@ fn fixture_engine_config() -> EngineConfig { forward_model: None, fpm_parquet_path: None, decoder_replay: false, + moe_kernel_source: None, kv_block_size: None, parallel: ParallelMapping { tp_size: 8, diff --git a/crates/core/src/perfmodel/memory.rs b/crates/core/src/perfmodel/memory.rs index c30377599a..ed44c2ef12 100644 --- a/crates/core/src/perfmodel/memory.rs +++ b/crates/core/src/perfmodel/memory.rs @@ -509,6 +509,7 @@ mod tests { forward_model: None, fpm_parquet_path: None, decoder_replay: false, + moe_kernel_source: None, kv_block_size: None, parallel: ParallelMapping { tp_size: 1, diff --git a/crates/core/src/perfmodel/operators/fpm_sol.rs b/crates/core/src/perfmodel/operators/fpm_sol.rs index ddc48ba4d6..b89d2504cf 100644 --- a/crates/core/src/perfmodel/operators/fpm_sol.rs +++ b/crates/core/src/perfmodel/operators/fpm_sol.rs @@ -831,6 +831,7 @@ mod tests { workload_distribution: "uniform".into(), is_gated: true, moe_backend: None, + moe_kernel_source: None, enable_eplb: false, is_context: true, }; diff --git a/crates/core/src/perfmodel/operators/moe.rs b/crates/core/src/perfmodel/operators/moe.rs index 35dbef00f1..2c870793ea 100644 --- a/crates/core/src/perfmodel/operators/moe.rs +++ b/crates/core/src/perfmodel/operators/moe.rs @@ -44,7 +44,7 @@ use crate::operators::base::{PerformanceResult, SolComponents, Source}; use crate::operators::util_empirical::{self, UtilGrid}; use crate::perf_database::PerfDatabase; use crate::perf_database::gemm::quant_tc_flops; -use crate::perf_database::moe::{MoeKernel, MoeSiblingSlice}; +use crate::perf_database::moe::MoeSiblingSlice; use serde::{Deserialize, Serialize}; use std::sync::Arc; @@ -144,30 +144,24 @@ pub(crate) fn policy_fingerprint(policy: TransferPolicy) -> String { /// to the wideep context/generation MoE tables retired with AIC-1601 — see /// the typed error raised at each selector point.) #[derive(Clone, Copy, PartialEq)] -enum MoeTableSel { +enum MoeTableSel<'a> { Standard, LowLatency, + ExactKernelSource(&'a str), } -impl MoeTableSel { - /// Grid cache-key tag. Python folds `kernel_tag` ("std" / "ll") plus - /// `id(node)` into the key. - fn tag(self) -> &'static str { +impl MoeTableSel<'_> { + /// Grid cache-key tag. Exact source selection must be included so a grid + /// built from one kernel lane can never answer for another. + fn tag(self) -> String { match self { - Self::Standard => "std", - Self::LowLatency => "ll", + Self::Standard => "std".to_owned(), + Self::LowLatency => "ll".to_owned(), + Self::ExactKernelSource(source) => format!("kernel_source={source}"), } } } -/// The perf-DB kernel grid behind a selector. -fn moe_kernel(table: MoeTableSel) -> MoeKernel { - match table { - MoeTableSel::Standard => MoeKernel::Standard, - MoeTableSel::LowLatency => MoeKernel::LowLatency, - } -} - /// A sibling slice the transfer ladder may borrow: the reference slice's /// shape + token curve, the quant whose SOL reconstructs its util /// (`sol_quant`: QUERY quant for same-profile tiers, REFERENCE quant for @@ -214,6 +208,11 @@ pub struct MoeOp { /// pre-existing specs -> None -> the regular table. #[serde(default)] pub moe_backend: Option, + /// Exact collected MoE kernel-source lane. `None` preserves the + /// framework/default lookup; `Some` selects only rows carrying this exact + /// `kernel_source` value and fails on a missing lane. + #[serde(default)] + pub moe_kernel_source: Option, /// EPLB enabled (Python `MoE._enable_eplb`). On the sglang branch the /// PREFILL token count is corrected to `int(num_tokens * 0.8)` /// (operations/moe.py: expert-parallel load balancing evens the @@ -266,6 +265,7 @@ impl MoeOp { workload_distribution: workload_distribution.into(), is_gated: true, moe_backend: None, + moe_kernel_source: None, enable_eplb: false, is_context: false, } @@ -361,6 +361,29 @@ impl MoeOp { ))); } + if let Some(kernel_source) = self.moe_kernel_source.as_deref() { + let value = db.moe.query_kernel_source( + kernel_source, + num_tokens, + self.hidden_size, + self.inter_size, + self.topk, + self.num_experts, + self.moe_tp_size, + self.moe_ep_size, + self.quant_mode, + &self.workload_distribution, + &sol, + )?; + return Ok(PerformanceResult::with_energy( + value.latency, + value.energy, + Source::Silicon, + ) + .clamp_non_negative() + .scaled(self.scale_factor)); + } + // Mirrors Python's MoE._query_moe_table TRT-LLM gate: for nvfp4 // gated MoE at num_tokens <= 128, probe the // `moe_torch_flow_min_latency` grid first and fall back to the @@ -449,18 +472,20 @@ impl MoeOp { self.name ))); } - let table = if num_tokens <= 128 - && quant == MoeQuantMode::Nvfp4 - && self.is_gated - && db.moe.low_latency_available()? - { - match self.slice_points(db, MoeTableSel::LowLatency) { - Ok(_) => MoeTableSel::LowLatency, - Err(err) if err.is_missing_perf_data() => MoeTableSel::Standard, - Err(err) => return Err(err), + let table = match self.moe_kernel_source.as_deref() { + Some(kernel_source) => MoeTableSel::ExactKernelSource(kernel_source), + None if num_tokens <= 128 + && quant == MoeQuantMode::Nvfp4 + && self.is_gated + && db.moe.low_latency_available()? => + { + match self.slice_points(db, MoeTableSel::LowLatency) { + Ok(_) => MoeTableSel::LowLatency, + Err(err) if err.is_missing_perf_data() => MoeTableSel::Standard, + Err(err) => return Err(err), + } } - } else { - MoeTableSel::Standard + None => MoeTableSel::Standard, }; let kernel_tag = table.tag(); @@ -535,7 +560,7 @@ impl MoeOp { } } if let Some(reference) = - self.reference_grid(db, kernel_tag, num_gemms, policy, &candidates)? + self.reference_grid(db, &kernel_tag, num_gemms, policy, &candidates)? { grid = Some(reference); } @@ -563,7 +588,7 @@ impl MoeOp { &mut candidates, )?; if let Some(reference) = - self.reference_grid(db, kernel_tag, num_gemms, policy, &candidates)? + self.reference_grid(db, &kernel_tag, num_gemms, policy, &candidates)? { if !reference.is_empty() { grid = Some(reference); @@ -594,19 +619,43 @@ impl MoeOp { fn slice_points( &self, db: &PerfDatabase, - table: MoeTableSel, + table: MoeTableSel<'_>, ) -> Result, AicError> { - db.moe.slice_points( - moe_kernel(table), - self.quant_mode.name(), - &self.workload_distribution, - self.topk, - self.num_experts, - self.hidden_size, - self.inter_size, - self.moe_tp_size, - self.moe_ep_size, - ) + match table { + MoeTableSel::Standard => db.moe.slice_points( + crate::perf_database::moe::MoeKernel::Standard, + self.quant_mode.name(), + &self.workload_distribution, + self.topk, + self.num_experts, + self.hidden_size, + self.inter_size, + self.moe_tp_size, + self.moe_ep_size, + ), + MoeTableSel::LowLatency => db.moe.slice_points( + crate::perf_database::moe::MoeKernel::LowLatency, + self.quant_mode.name(), + &self.workload_distribution, + self.topk, + self.num_experts, + self.hidden_size, + self.inter_size, + self.moe_tp_size, + self.moe_ep_size, + ), + MoeTableSel::ExactKernelSource(kernel_source) => db.moe.slice_points_for_kernel_source( + kernel_source, + self.quant_mode.name(), + &self.workload_distribution, + self.topk, + self.num_experts, + self.hidden_size, + self.inter_size, + self.moe_tp_size, + self.moe_ep_size, + ), + } } /// Distinct quant names of the selected table, in first-seen (file row) @@ -614,9 +663,19 @@ impl MoeOp { fn table_available_quants( &self, db: &PerfDatabase, - table: MoeTableSel, + table: MoeTableSel<'_>, ) -> Result, AicError> { - db.moe.available_quants(moe_kernel(table)) + match table { + MoeTableSel::Standard => db + .moe + .available_quants(crate::perf_database::moe::MoeKernel::Standard), + MoeTableSel::LowLatency => db + .moe + .available_quants(crate::perf_database::moe::MoeKernel::LowLatency), + MoeTableSel::ExactKernelSource(kernel_source) => { + db.moe.available_quants_for_kernel_source(kernel_source) + } + } } /// Enumerate `source_quant`'s collected sibling slices (same table, @@ -627,19 +686,37 @@ impl MoeOp { fn collect_candidates( &self, db: &PerfDatabase, - table: MoeTableSel, + table: MoeTableSel<'_>, source_quant: MoeQuantMode, sol_quant: MoeQuantMode, provenance: &'static str, out: &mut Vec, ) -> Result<(), AicError> { - let slices = db.moe.sibling_slices( - moe_kernel(table), - source_quant.name(), - &self.workload_distribution, - self.moe_tp_size, - self.moe_ep_size, - ); + let slices = match table { + MoeTableSel::Standard => db.moe.sibling_slices( + crate::perf_database::moe::MoeKernel::Standard, + source_quant.name(), + &self.workload_distribution, + self.moe_tp_size, + self.moe_ep_size, + ), + MoeTableSel::LowLatency => db.moe.sibling_slices( + crate::perf_database::moe::MoeKernel::LowLatency, + source_quant.name(), + &self.workload_distribution, + self.moe_tp_size, + self.moe_ep_size, + ), + MoeTableSel::ExactKernelSource(kernel_source) => { + db.moe.sibling_slices_for_kernel_source( + kernel_source, + source_quant.name(), + &self.workload_distribution, + self.moe_tp_size, + self.moe_ep_size, + ) + } + }; let slices = match slices { Ok(slices) => slices, Err(err) if err.is_missing_perf_data() => return Ok(()), @@ -874,6 +951,7 @@ mod tests { attention_dp_size, is_gated: true, moe_backend: None, + moe_kernel_source: None, enable_eplb: false, is_context: false, } @@ -911,6 +989,7 @@ mod tests { attention_dp_size: 1, is_gated: true, moe_backend: None, + moe_kernel_source: None, enable_eplb: false, is_context: false, } @@ -1127,6 +1206,7 @@ mod tests { attention_dp_size: 1, is_gated: true, moe_backend: None, + moe_kernel_source: None, enable_eplb: false, is_context: false, }; @@ -1156,6 +1236,7 @@ mod tests { attention_dp_size: 1, is_gated: true, moe_backend: None, + moe_kernel_source: None, enable_eplb: false, is_context: false, }; diff --git a/crates/core/src/perfmodel/operators/moe_expert_compute.rs b/crates/core/src/perfmodel/operators/moe_expert_compute.rs index bdb6d9c2d3..15bf976c90 100644 --- a/crates/core/src/perfmodel/operators/moe_expert_compute.rs +++ b/crates/core/src/perfmodel/operators/moe_expert_compute.rs @@ -202,6 +202,7 @@ impl MoeExpertComputeOp { workload_distribution: self.workload_distribution.clone(), is_gated: self.is_gated, moe_backend: None, + moe_kernel_source: None, enable_eplb: false, is_context: self.inference_phase == "context", }; diff --git a/crates/core/src/perfmodel/perf_database/moe.rs b/crates/core/src/perfmodel/perf_database/moe.rs index 8a5587f3e5..52e5013683 100644 --- a/crates/core/src/perfmodel/perf_database/moe.rs +++ b/crates/core/src/perfmodel/perf_database/moe.rs @@ -74,14 +74,13 @@ pub struct MoeSiblingSlice { pub points: Vec<(u32, f64)>, } -/// Two parallel grids split by `kernel_source`. Mirrors Python's split in -/// `aisimulate.sdk.operations.moe.MoE.load_data`, where rows tagged -/// `kernel_source == "moe_torch_flow_min_latency"` route to a separate -/// accumulator that the TRT-LLM SILICON path probes first for small-token -/// nvfp4 gated MoE queries. +/// Parallel grids split by `kernel_source`. `default` and `low_latency` +/// preserve the legacy automatic lookup behavior, while `by_kernel_source` +/// retains every named lane for an exact user-selected lookup. struct LoadedMoeGrids { default: MoeGrids, low_latency: MoeGrids, + by_kernel_source: BTreeMap, } struct MoeGrids { @@ -94,6 +93,29 @@ struct MoeGrids { quants_in_load_order: Vec, } +fn insert_moe_row( + index: &mut MoeIndex>, + quants_in_load_order: &mut Vec, + quant: &str, + distribution: &str, + shape: MoeShapeKey, + num_tokens: u32, + value: LeafValue, +) { + if !quants_in_load_order + .iter() + .any(|existing| existing == quant) + { + quants_in_load_order.push(quant.to_owned()); + } + // First-seen value wins within both the legacy merged grid and an exact + // lane, including across priority-ordered shared-layer sources. + index + .entry(quant.to_owned(), distribution.to_owned(), shape) + .entry(num_tokens) + .or_insert(value); +} + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] struct MoeKey { quant: String, @@ -167,8 +189,70 @@ impl MoeTable { workload_distribution: &str, sol: &dyn Fn(f64) -> f64, ) -> Result { - let loaded = self.load()?; - let grids = &loaded.default; + self.query_for_kernel_source( + None, + num_tokens, + hidden_size, + inter_size, + topk, + num_experts, + moe_tp_size, + moe_ep_size, + quant, + workload_distribution, + sol, + ) + } + + /// Query one exact collected kernel-source lane. The requested lane is + /// never merged with or substituted by another lane: an unavailable lane + /// is a typed missing-data error instead of a silent fallback. + #[allow(clippy::too_many_arguments)] + pub fn query_kernel_source( + &self, + kernel_source: &str, + num_tokens: u32, + hidden_size: u32, + inter_size: u32, + topk: u32, + num_experts: u32, + moe_tp_size: u32, + moe_ep_size: u32, + quant: MoeQuantMode, + workload_distribution: &str, + sol: &dyn Fn(f64) -> f64, + ) -> Result { + self.query_for_kernel_source( + Some(kernel_source), + num_tokens, + hidden_size, + inter_size, + topk, + num_experts, + moe_tp_size, + moe_ep_size, + quant, + workload_distribution, + sol, + ) + } + + #[allow(clippy::too_many_arguments)] + fn query_for_kernel_source( + &self, + kernel_source: Option<&str>, + num_tokens: u32, + hidden_size: u32, + inter_size: u32, + topk: u32, + num_experts: u32, + moe_tp_size: u32, + moe_ep_size: u32, + quant: MoeQuantMode, + workload_distribution: &str, + sol: &dyn Fn(f64) -> f64, + ) -> Result { + let grids = self.grids_for_kernel_source(kernel_source)?; let quant_name = quant.name(); let shape = MoeShapeKey { @@ -301,6 +385,63 @@ impl MoeTable { moe_ep_size: u32, ) -> Result, AicError> { let grids = self.grids_for(kernel)?; + self.slice_points_in_grids( + grids, + format!("{kernel:?}"), + quant_name, + workload_distribution, + topk, + num_experts, + hidden_size, + inter_size, + moe_tp_size, + moe_ep_size, + ) + } + + /// Own-slice token curve from an exact collected kernel-source lane. + #[allow(clippy::too_many_arguments)] + pub fn slice_points_for_kernel_source( + &self, + kernel_source: &str, + quant_name: &str, + workload_distribution: &str, + topk: u32, + num_experts: u32, + hidden_size: u32, + inter_size: u32, + moe_tp_size: u32, + moe_ep_size: u32, + ) -> Result, AicError> { + let grids = self.grids_for_kernel_source(Some(kernel_source))?; + self.slice_points_in_grids( + grids, + format!("kernel_source={kernel_source:?}"), + quant_name, + workload_distribution, + topk, + num_experts, + hidden_size, + inter_size, + moe_tp_size, + moe_ep_size, + ) + } + + #[allow(clippy::too_many_arguments)] + fn slice_points_in_grids( + &self, + grids: &MoeGrids, + grid_label: String, + quant_name: &str, + workload_distribution: &str, + topk: u32, + num_experts: u32, + hidden_size: u32, + inter_size: u32, + moe_tp_size: u32, + moe_ep_size: u32, + ) -> Result, AicError> { let shape = MoeShapeKey { topk, num_experts, @@ -316,7 +457,7 @@ impl MoeTable { let by_tokens = by_tokens.filter(|curve| !curve.is_empty()).ok_or_else(|| { let key = MoeKey::from_shape(quant_name, dist, shape); AicError::PerfDatabase(format!( - "MoE data missing for {key:?} ({kernel:?}) at {}", + "MoE data missing for {key:?} ({grid_label}) at {}", self.data_root.display() )) })?; @@ -343,12 +484,47 @@ impl MoeTable { moe_ep_size: u32, ) -> Result, AicError> { let grids = self.grids_for(kernel)?; + Ok(Self::sibling_slices_in_grids( + grids, + quant_name, + workload_distribution, + moe_tp_size, + moe_ep_size, + )) + } + + /// Collected sibling slices from an exact kernel-source lane. + pub fn sibling_slices_for_kernel_source( + &self, + kernel_source: &str, + quant_name: &str, + workload_distribution: &str, + moe_tp_size: u32, + moe_ep_size: u32, + ) -> Result, AicError> { + let grids = self.grids_for_kernel_source(Some(kernel_source))?; + Ok(Self::sibling_slices_in_grids( + grids, + quant_name, + workload_distribution, + moe_tp_size, + moe_ep_size, + )) + } + + fn sibling_slices_in_grids( + grids: &MoeGrids, + quant_name: &str, + workload_distribution: &str, + moe_tp_size: u32, + moe_ep_size: u32, + ) -> Vec { let (_, by_shape) = grids .index .resolve_uniform_shapes(quant_name, workload_distribution); let mut slices = Vec::new(); let Some(by_shape) = by_shape else { - return Ok(slices); + return slices; }; for (shape, curve) in by_shape { if shape.moe_tp_size != moe_tp_size @@ -365,7 +541,7 @@ impl MoeTable { points: curve.iter().map(|(t, leaf)| (t, leaf.latency)).collect(), }); } - Ok(slices) + slices } /// Distinct quant names present in the kernel grid, in first-seen @@ -376,6 +552,17 @@ impl MoeTable { Ok(self.grids_for(kernel)?.quants_in_load_order.clone()) } + /// Distinct quant names in an exact collected kernel-source lane. + pub fn available_quants_for_kernel_source( + &self, + kernel_source: &str, + ) -> Result, AicError> { + Ok(self + .grids_for_kernel_source(Some(kernel_source))? + .quants_in_load_order + .clone()) + } + fn grids_for(&self, kernel: MoeKernel) -> Result<&MoeGrids, AicError> { let loaded = self.load()?; Ok(match kernel { @@ -384,6 +571,19 @@ impl MoeTable { }) } + fn grids_for_kernel_source(&self, kernel_source: Option<&str>) -> Result<&MoeGrids, AicError> { + let loaded = self.load()?; + match kernel_source { + None => Ok(&loaded.default), + Some(kernel_source) => loaded.by_kernel_source.get(kernel_source).ok_or_else(|| { + AicError::PerfDatabase(format!( + "MoE kernel_source {kernel_source:?} is unavailable at {}", + self.data_root.display() + )) + }), + } + } + fn load(&self) -> Result<&LoadedMoeGrids, AicError> { let cell = self.moe.get_or_init(|| load_moe_parquet(&self.moe_sources)); cell.as_ref().map_err(clone_err) @@ -417,6 +617,11 @@ fn load_moe_parquet(sources: &[PerfSource]) -> Result MoeIndex::default(); let mut default_quants: Vec = Vec::new(); let mut low_latency_quants: Vec = Vec::new(); + let mut kernel_source_indices: BTreeMap< + String, + MoeIndex>, + > = BTreeMap::new(); + let mut kernel_source_quants: BTreeMap> = BTreeMap::new(); let mut any_source = false; for source in sources { let path = source.path(); @@ -474,22 +679,34 @@ fn load_moe_parquet(sources: &[PerfSource]) -> Result } else { (&mut default_index, &mut default_quants) }; - // First-seen (file row) quant order — Python's dict insertion - // order, consumed by `available_quants`. - if !target_quants.iter().any(|q| q == &quant) { - target_quants.push(quant.clone()); - } let latency = row.f64(latency_col)?; let power = row.f64_optional(power_col)?.unwrap_or(0.0); - // Python's `load_moe_data` wraps the leaf insert in a try/except KeyError - // and skips on conflict, i.e. it keeps the FIRST occurrence of each - // (shape, num_tokens) tuple. Some perf files contain duplicate rows - // (same kernel_source, same shape) — preserving first-wins parity here, - // extended across shared-layer sources (earlier source wins). - target - .entry(quant, distribution, shape) - .entry(row.u32(num_tokens_col)?) - .or_insert(LeafValue::with_power(latency, power)); + let value = LeafValue::with_power(latency, power); + let num_tokens = row.u32(num_tokens_col)?; + insert_moe_row( + target, + target_quants, + &quant, + &distribution, + shape, + num_tokens, + value, + ); + if !kernel_source.is_empty() { + let source_index = kernel_source_indices + .entry(kernel_source.clone()) + .or_default(); + let source_quants = kernel_source_quants.entry(kernel_source).or_default(); + insert_moe_row( + source_index, + source_quants, + &quant, + &distribution, + shape, + num_tokens, + value, + ); + } } } if !any_source || (default_index.is_empty() && low_latency_index.is_empty()) { @@ -502,6 +719,21 @@ fn load_moe_parquet(sources: &[PerfSource]) -> Result .unwrap_or_default() ))); } + let by_kernel_source = kernel_source_indices + .into_iter() + .map(|(kernel_source, index)| { + let quants_in_load_order = kernel_source_quants + .remove(&kernel_source) + .expect("kernel-source quant order is inserted with its index"); + ( + kernel_source, + MoeGrids { + index: index.map_values(|curve| LeafAxisCurve::from_map("num_tokens", curve)), + quants_in_load_order, + }, + ) + }) + .collect(); Ok(LoadedMoeGrids { default: MoeGrids { index: default_index.map_values(|curve| LeafAxisCurve::from_map("num_tokens", curve)), @@ -512,6 +744,7 @@ fn load_moe_parquet(sources: &[PerfSource]) -> Result .map_values(|curve| LeafAxisCurve::from_map("num_tokens", curve)), quants_in_load_order: low_latency_quants, }, + by_kernel_source, }) } @@ -678,6 +911,85 @@ mod tests { ); } + #[test] + fn moe_exact_kernel_source_selects_the_requested_sglang_lane() { + // Both lanes cover the same FP8 MoE shape. Unset retains the legacy + // first-row/default result; an exact selector must use only its lane. + use crate::perf_database::energy_test_fixtures::{Col, write_parquet}; + + let tmp = tempfile::tempdir().expect("tmpdir"); + write_parquet( + &tmp.path().join("moe_perf.parquet"), + &[ + Col::Str("moe_dtype", vec!["fp8_block", "fp8_block"]), + Col::I64("num_tokens", vec![1024, 1024]), + Col::I64("hidden_size", vec![8192, 8192]), + Col::I64("inter_size", vec![2048, 2048]), + Col::I64("topk", vec![10, 10]), + Col::I64("num_experts", vec![512, 512]), + Col::I64("moe_tp_size", vec![1, 1]), + Col::I64("moe_ep_size", vec![8, 8]), + Col::Str("distribution", vec!["power_law_1.2", "power_law_1.2"]), + Col::Str( + "kernel_source", + vec!["sglang_fused_moe_triton", "sglang_flashinfer_trtllm_moe"], + ), + Col::F64("latency", vec![1.0, 0.25]), + ], + ); + let table = MoeTable::new(tmp.path().to_path_buf()); + + let legacy = table + .query( + 1024, + 8192, + 2048, + 10, + 512, + 1, + 8, + MoeQuantMode::Fp8Block, + "power_law_1.2", + &proxy_sol, + ) + .expect("default lane resolves"); + let flashinfer = table + .query_kernel_source( + "sglang_flashinfer_trtllm_moe", + 1024, + 8192, + 2048, + 10, + 512, + 1, + 8, + MoeQuantMode::Fp8Block, + "power_law_1.2", + &proxy_sol, + ) + .expect("requested FlashInfer lane resolves"); + + assert_eq!(legacy.latency, 1.0); + assert_eq!(flashinfer.latency, 0.25); + + let error = table + .query_kernel_source( + "sglang_missing_moe", + 1024, + 8192, + 2048, + 10, + 512, + 1, + 8, + MoeQuantMode::Fp8Block, + "power_law_1.2", + &proxy_sol, + ) + .expect_err("an unavailable exact lane must not fall back to the default"); + assert!(error.to_string().contains("sglang_missing_moe")); + } + /// ENERGY oracle on a synthetic power-carrying fixture. Python twin /// (pandas fixture, `energy_test_fixtures` spec): /// diff --git a/crates/core/src/perfmodel/py.rs b/crates/core/src/perfmodel/py.rs index 933a04e08a..692b70031b 100644 --- a/crates/core/src/perfmodel/py.rs +++ b/crates/core/src/perfmodel/py.rs @@ -1050,6 +1050,7 @@ struct EngineBuildRequest { moe_backend: Option, enable_eplb: bool, wideep_num_slots: Option, + moe_kernel_source: Option, nextn: u32, speculation: Option, kv_block_size: Option, @@ -1100,6 +1101,7 @@ impl AicEngineBuilder { moe_backend: None, enable_eplb: false, wideep_num_slots: None, + moe_kernel_source: None, nextn: 0, speculation: None, kv_block_size: None, @@ -1225,6 +1227,12 @@ impl AicEngineBuilder { self } + /// Select an exact collected MoE compute kernel-source lane. + pub fn moe_kernel_source(mut self, value: impl Into) -> Self { + self.request.moe_kernel_source = Some(value.into()); + self + } + /// Configure speculative decoding. pub fn speculative_decoding(mut self, nextn: u32) -> Self { self.request.nextn = nextn; @@ -1264,6 +1272,7 @@ mod builder_tests { assert!(builder.request.moe_tp_size.is_none()); assert!(builder.request.moe_ep_size.is_none()); assert!(builder.request.attention_backend.is_none()); + assert!(builder.request.moe_kernel_source.is_none()); assert!(builder.request.kv_block_size.is_none()); assert!(builder.request.fpm_parquet_path.is_none()); assert!(builder.request.database_mode.is_none()); @@ -1281,6 +1290,7 @@ mod builder_tests { .attention_dp_size(4) .moe_parallelism(Some(1), Some(8)) .attention_backend("fa3") + .moe_kernel_source("sglang_flashinfer_trtllm_moe") .database_mode(DatabaseMode::Empirical) .shared_layer(true) .transfer_policy(vec!["xshape".to_owned(), "xquant".to_owned()]) @@ -1298,6 +1308,10 @@ mod builder_tests { (Some(1), Some(8)) ); assert_eq!(builder.request.attention_backend.as_deref(), Some("fa3")); + assert_eq!( + builder.request.moe_kernel_source.as_deref(), + Some("sglang_flashinfer_trtllm_moe") + ); assert_eq!(builder.request.database_mode.as_deref(), Some("EMPIRICAL")); assert_eq!(builder.request.shared_layer, Some(true)); assert_eq!( @@ -1434,6 +1448,7 @@ fn compile_engine_from_request(request: EngineBuildRequest) -> Result, + moe_kernel_source: Option, enable_eplb: bool, seq_split: u32, ) -> PyResult<(Self, PyOperation)> { @@ -1799,6 +1800,7 @@ impl PyMoE { workload_distribution, is_gated, moe_backend, + moe_kernel_source, enable_eplb, is_context, }); @@ -1828,6 +1830,7 @@ impl PyMoE { kwargs.set_item("is_context", o.is_context)?; kwargs.set_item("is_gated", o.is_gated)?; kwargs.set_item("moe_backend", o.moe_backend.clone())?; + kwargs.set_item("moe_kernel_source", o.moe_kernel_source.clone())?; kwargs.set_item("enable_eplb", o.enable_eplb)?; Ok((args, kwargs)) } @@ -1896,6 +1899,11 @@ impl PyMoE { Ok(slf.as_super().moe()?.moe_backend.clone()) } + #[getter(_moe_kernel_source)] + fn moe_kernel_source(slf: PyRef<'_, Self>) -> PyResult> { + Ok(slf.as_super().moe()?.moe_kernel_source.clone()) + } + #[getter(_enable_eplb)] fn enable_eplb(slf: PyRef<'_, Self>) -> PyResult { Ok(slf.as_super().moe()?.enable_eplb) diff --git a/crates/core/src/python.rs b/crates/core/src/python.rs index 2992aae5e8..771f43b65e 100644 --- a/crates/core/src/python.rs +++ b/crates/core/src/python.rs @@ -220,6 +220,8 @@ struct AicTimingConfig { #[serde(default)] attention_backend: Option, #[serde(default)] + moe_kernel_source: Option, + #[serde(default)] moe_backend: Option, #[serde(default)] enable_eplb: bool, @@ -315,6 +317,7 @@ impl AicTimingConfig { moe_backend: self.moe_backend.clone(), enable_eplb: self.enable_eplb, wideep_num_slots: self.wideep_num_slots, + moe_kernel_source: self.moe_kernel_source.clone(), enable_shared_layer: self.enable_shared_layer, strict_provenance: self.strict_provenance, }) @@ -2248,6 +2251,21 @@ mod tests { ); } + #[test] + fn canonical_timing_config_preserves_moe_kernel_source() { + let mut config = aic_config(); + config.moe_kernel_source = Some("sglang_flashinfer_trtllm_moe".into()); + + assert_eq!( + config + .estimator_request(ForwardPassWorkerType::Aggregated) + .unwrap() + .moe_kernel_source + .as_deref(), + Some("sglang_flashinfer_trtllm_moe") + ); + } + fn aic_config() -> AicTimingConfig { AicTimingConfig { model: "test-model".into(), @@ -2282,6 +2300,7 @@ mod tests { moe_backend: None, enable_eplb: false, wideep_num_slots: None, + moe_kernel_source: None, enable_shared_layer: None, strict_provenance: false, systems_path: None, diff --git a/python/aisimulate/src/aisimulate/capacity.py b/python/aisimulate/src/aisimulate/capacity.py index 00b5f1f5aa..60e0de2944 100644 --- a/python/aisimulate/src/aisimulate/capacity.py +++ b/python/aisimulate/src/aisimulate/capacity.py @@ -129,7 +129,7 @@ def finish_lowering(value): ): if resolved[source] is not None: lowered[target] = resolved[source] - for name in ("moe_backend", "attention_backend", "enable_eplb", "wideep_num_slots"): + for name in ("moe_backend", "moe_kernel_source", "attention_backend", "enable_eplb", "wideep_num_slots"): if resolved.get(name) is not None and not (name == "enable_eplb" and resolved[name] is False): lowered[f"aic_{name}"] = resolved[name] if resolved["systems_paths"]: @@ -208,7 +208,7 @@ def finish_lowering(value): comm_dtype=lowered.get("aic_comm_dtype"), **{ name: lowered[f"aic_{name}"] - for name in ("moe_backend", "attention_backend", "enable_eplb", "wideep_num_slots") + for name in ("moe_backend", "moe_kernel_source", "attention_backend", "enable_eplb", "wideep_num_slots") if lowered.get(f"aic_{name}") is not None }, systems_path=capacity_systems_path, @@ -241,6 +241,7 @@ def estimate_num_gpu_blocks( kv_cache_dtype: str | None = None, comm_dtype: str | None = None, moe_backend: str | None = None, + moe_kernel_source: str | None = None, attention_backend: str | None = None, enable_eplb: bool = False, wideep_num_slots: int | None = None, @@ -315,6 +316,7 @@ def estimate_num_gpu_blocks( name: value for name, value in ( ("moe_backend", moe_backend), + ("moe_kernel_source", moe_kernel_source), ("attention_backend", attention_backend), ("enable_eplb", enable_eplb), ("wideep_num_slots", wideep_num_slots), diff --git a/python/aisimulate/src/aisimulate/config/common.py b/python/aisimulate/src/aisimulate/config/common.py index 442798668a..3a5d59da23 100644 --- a/python/aisimulate/src/aisimulate/config/common.py +++ b/python/aisimulate/src/aisimulate/config/common.py @@ -224,6 +224,7 @@ def split_config_sections( "enable_eplb", "wideep_num_slots", "moe_backend", + "moe_kernel_source", "attention_backend", "gemm_quant_mode", "moe_quant_mode", @@ -247,7 +248,7 @@ def is_active_engine_model_control(name: str, value: Any) -> bool: def omit_inactive_moe_controls(config: dict[str, Any]) -> dict[str, Any]: """Keep additive defaults out of timing payloads parsed by older runners.""" result = dict(config) - for name in ("moe_backend", "wideep_num_slots", "enable_eplb"): + for name in ("moe_backend", "moe_kernel_source", "wideep_num_slots", "enable_eplb"): if not is_active_engine_model_control(name, result.get(name)): result.pop(name, None) return result diff --git a/python/aisimulate/src/aisimulate/config/engine.py b/python/aisimulate/src/aisimulate/config/engine.py index 8869bc4570..8e811b19fc 100644 --- a/python/aisimulate/src/aisimulate/config/engine.py +++ b/python/aisimulate/src/aisimulate/config/engine.py @@ -273,6 +273,7 @@ class EstimatorPolicyConfig(StrictModel): enable_eplb: bool = Field(default=False, strict=True) wideep_num_slots: PositiveInt | None = None moe_backend: str | None = None + moe_kernel_source: str | None = None attention_backend: str | None = None gemm_quant_mode: str | None = None moe_quant_mode: str | None = None diff --git a/python/aisimulate/src/aisimulate/runner.py b/python/aisimulate/src/aisimulate/runner.py index e093292b48..8c145d5e97 100644 --- a/python/aisimulate/src/aisimulate/runner.py +++ b/python/aisimulate/src/aisimulate/runner.py @@ -98,6 +98,7 @@ "forward_model": ("forward_model", "aic_forward_model"), "fpm_parquet_path": ("fpm_parquet_path", "aic_fpm_parquet_path"), "moe_backend": ("aic_moe_backend",), + "moe_kernel_source": ("moe_kernel_source", "aic_moe_kernel_source"), "attention_backend": ("aic_attention_backend",), "enable_eplb": ("aic_enable_eplb",), "wideep_num_slots": ("aic_wideep_num_slots",), diff --git a/python/aisimulate/src/aisimulate/sdk/task_v1_compat.py b/python/aisimulate/src/aisimulate/sdk/task_v1_compat.py index f383978eb3..7d1158e4f1 100644 --- a/python/aisimulate/src/aisimulate/sdk/task_v1_compat.py +++ b/python/aisimulate/src/aisimulate/sdk/task_v1_compat.py @@ -86,13 +86,14 @@ "nextn_accept_rates", "moe_backend", "attention_backend", + "moe_kernel_source", "wideep_num_slots", } ) # Global scalars that may appear inside the V1 ``config:`` block. _CONFIG_SCALARS: frozenset[str] = frozenset( - {"moe_backend", "nextn", "nextn_accept_rates", "attention_backend", "wideep_num_slots"} + {"moe_backend", "nextn", "nextn_accept_rates", "attention_backend", "moe_kernel_source", "wideep_num_slots"} ) # Keys that are part of the V1 format itself (handled structurally, never copied). diff --git a/python/aisimulate/src/aisimulate/sdk/task_v2.py b/python/aisimulate/src/aisimulate/sdk/task_v2.py index 00ad270b0a..a24afc41a3 100644 --- a/python/aisimulate/src/aisimulate/sdk/task_v2.py +++ b/python/aisimulate/src/aisimulate/sdk/task_v2.py @@ -576,6 +576,9 @@ class Task: # closed; None/default uses the mapped framework default or safe default fallback. SGLang WideEP # maps None/default to flashinfer and also supports fa3. attention_backend: common.AttentionBackend | None = None + # Exact collected MoE compute kernel-source lane; unlike moe_backend this + # does not select topology or a runtime implementation family. + moe_kernel_source: str | None = field(default=None, kw_only=True) wideep_num_slots: int | None = None # EPLB slot count; defaults to num_experts when None gemm_quant_mode: common.GEMMQuantMode | None = None moe_quant_mode: common.MoEQuantMode | None = None @@ -860,6 +863,7 @@ def __post_init__(self) -> None: common.AttentionBackend, "attention_backend", ) + self.moe_kernel_source = config.normalize_kernel_source(self.moe_kernel_source, "moe_kernel_source") # Canonicalize at construction so downstream config and routing see # exactly one spelling for the only supported engine-step backend. self.engine_step_backend = validate_engine_step_backend(self.engine_step_backend) @@ -2125,6 +2129,7 @@ def build_model_config( # this field as the kernel-LANE override — materializing a lane name here # would silently pin every model to the flashinfer lane. attention_backend=self.attention_backend, + moe_kernel_source=self.moe_kernel_source, wideep_num_slots=self.wideep_num_slots, forward_model=self.forward_model or "op_level", moe_comm_backend=None, diff --git a/python/aisimulate/src/aisimulate/sweeper/config.py b/python/aisimulate/src/aisimulate/sweeper/config.py index c2ab78f0a4..0c68ec00ea 100644 --- a/python/aisimulate/src/aisimulate/sweeper/config.py +++ b/python/aisimulate/src/aisimulate/sweeper/config.py @@ -539,6 +539,7 @@ class SearchSpace(BaseModel): enable_eplb: bool = Field(default=False, strict=True) wideep_num_slots: int | None = Field(default=None, strict=True, gt=0) moe_backend: str | None = None + moe_kernel_source: str | None = None attention_backend: str | None = None gemm_quant_mode: str | None = None moe_quant_mode: str | None = None diff --git a/python/aisimulate/src/aisimulate_core/sdk/config.py b/python/aisimulate/src/aisimulate_core/sdk/config.py index 72d01e9c62..cc59d51e37 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/config.py +++ b/python/aisimulate/src/aisimulate_core/sdk/config.py @@ -28,6 +28,15 @@ def normalize_kernel_backend( raise ValueError(f"{field_name} must be one of {choices}, got {value!r}.") from exc +def normalize_kernel_source(value: str | None, field_name: str) -> str | None: + """Validate an optional exact collected kernel-source label.""" + if value is None: + return None + if not isinstance(value, str) or not value: + raise ValueError(f"{field_name} must be a non-empty string or None, got {value!r}.") + return value + + def has_video_input( *, num_videos: int = 0, @@ -108,6 +117,11 @@ class ModelConfig: # (backend, version, sm_version) wins. Do NOT default this to a lane name — # that would silently pin every model to that lane. attention_backend: common.AttentionBackend | None = None + # Exact collected MoE kernel-source lane. This is intentionally separate + # from ``moe_backend``, which describes topology/runtime behavior rather + # than a single measured compute-kernel lane. ``None`` preserves the + # existing framework/default lookup. + moe_kernel_source: str | None = field(default=None, kw_only=True) # DEPRECATED and ignored (large-EP is selected per tuple via # moe_comm_backend); kept for a compatibility window because ModelConfig # is exported through the supported core SDK facade and removal breaks @@ -140,6 +154,7 @@ def __post_init__(self) -> None: common.AttentionBackend, "attention_backend", ) + self.moe_kernel_source = normalize_kernel_source(self.moe_kernel_source, "moe_kernel_source") def resolve_moe_parallelism(self) -> tuple[int, int]: """Resolve and validate MoE parallelism dimensions in-place. diff --git a/python/aisimulate/src/aisimulate_core/sdk/config_builders.py b/python/aisimulate/src/aisimulate_core/sdk/config_builders.py index ea413f7270..9171e2504a 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/config_builders.py +++ b/python/aisimulate/src/aisimulate_core/sdk/config_builders.py @@ -39,6 +39,7 @@ def build_model_config( forward_model: str | None = None, enable_encoder_dp: bool = True, attention_backend: str | None = None, + moe_kernel_source: str | None = None, speculation=None, moe_backend: str | None = None, enable_eplb: bool = False, @@ -63,6 +64,7 @@ def build_model_config( moe_backend=None if moe_backend == "default" else moe_backend, enable_eplb=enable_eplb, wideep_num_slots=wideep_num_slots, + moe_kernel_source=moe_kernel_source, speculation=speculation, ) diff --git a/python/aisimulate/src/aisimulate_core/sdk/engine.py b/python/aisimulate/src/aisimulate_core/sdk/engine.py index fd296951b0..9bd1facb2b 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/engine.py +++ b/python/aisimulate/src/aisimulate_core/sdk/engine.py @@ -321,6 +321,7 @@ def _engine_config_dict( # Rust side reloads the perf database from this string verbatim. "backend_version": _literal_backend_version(system, backend, backend_version, systems_path, database), "fpm_parquet_path": fpm_parquet_path, + "moe_kernel_source": getattr(cfg, "moe_kernel_source", None), "kv_block_size": kv_block_size, "decoder_replay": bool(getattr(cfg, "decoder_replay", False)), # ParallelMapping (flattened) @@ -421,6 +422,7 @@ def compile_engine( fmha_quant_mode: str | None = None, comm_quant_mode: str | None = None, attention_backend: str | None = None, + moe_kernel_source: str | None = None, moe_backend: str | None = None, enable_eplb: bool = False, wideep_num_slots: int | None = None, @@ -477,6 +479,7 @@ def compile_engine( comm_quant_mode=comm_quant_mode, forward_model=forward_model, attention_backend=attention_backend, + moe_kernel_source=moe_kernel_source, moe_backend=moe_backend, enable_eplb=enable_eplb, wideep_num_slots=wideep_num_slots, diff --git a/python/aisimulate/src/aisimulate_core/sdk/memory.py b/python/aisimulate/src/aisimulate_core/sdk/memory.py index 1e0115274c..01e6ee9064 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/memory.py +++ b/python/aisimulate/src/aisimulate_core/sdk/memory.py @@ -279,6 +279,7 @@ def from_request( fmha_quant_mode: str | None = None, comm_quant_mode: str | None = None, moe_backend: str | None = None, + moe_kernel_source: str | None = None, attention_backend: str | None = None, enable_eplb: bool = False, wideep_num_slots: int | None = None, @@ -323,6 +324,7 @@ def from_request( moe_quant_mode=moe_quant_mode, comm_quant_mode=comm_quant_mode, moe_backend=moe_backend, + moe_kernel_source=moe_kernel_source, attention_backend=attention_backend, enable_eplb=enable_eplb, wideep_num_slots=wideep_num_slots, @@ -1017,6 +1019,7 @@ def estimate_kv_cache( fmha_quant_mode: str | None = None, comm_quant_mode: str | None = None, moe_backend: str | None = None, + moe_kernel_source: str | None = None, attention_backend: str | None = None, enable_eplb: bool = False, wideep_num_slots: int | None = None, @@ -1111,6 +1114,7 @@ def estimate_kv_cache( fmha_quant_mode=fmha_quant_mode, comm_quant_mode=comm_quant_mode, moe_backend=moe_backend, + moe_kernel_source=moe_kernel_source, attention_backend=attention_backend, enable_eplb=enable_eplb, wideep_num_slots=wideep_num_slots, @@ -1123,6 +1127,7 @@ def estimate_kv_cache( or enable_eplb or wideep_num_slots is not None or moe_backend not in (None, "default") + or moe_kernel_source is not None or attention_backend is not None ): raise ValueError( @@ -1182,6 +1187,7 @@ def estimate_num_gpu_blocks( fmha_quant_mode: str | None = None, comm_quant_mode: str | None = None, moe_backend: str | None = None, + moe_kernel_source: str | None = None, attention_backend: str | None = None, enable_eplb: bool = False, wideep_num_slots: int | None = None, @@ -1244,6 +1250,7 @@ def estimate_num_gpu_blocks( fmha_quant_mode=fmha_quant_mode, comm_quant_mode=comm_quant_mode, moe_backend=moe_backend, + moe_kernel_source=moe_kernel_source, attention_backend=attention_backend, enable_eplb=enable_eplb, wideep_num_slots=wideep_num_slots, diff --git a/python/aisimulate/src/aisimulate_core/sdk/models/blocks/moe.py b/python/aisimulate/src/aisimulate_core/sdk/models/blocks/moe.py index 6cf205cd3f..4538baae67 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/models/blocks/moe.py +++ b/python/aisimulate/src/aisimulate_core/sdk/models/blocks/moe.py @@ -296,6 +296,8 @@ def _default_moe_block_ops( # attribute keep working; absent/uncovered phase means the fused path below. comm_backend = (getattr(cfg, "moe_comm_backend", None) or {}).get(inference_phase) if comm_backend: + if getattr(cfg, "moe_kernel_source", None) is not None: + raise ValueError("moe_kernel_source is unsupported when moe_comm_backend selects the large-EP MoE graph") return _large_ep_block_ops( comm_backend, prefix=prefix, @@ -397,6 +399,7 @@ def _default_moe_block_ops( quant_mode, workload_distribution, cfg.attention_dp_size, + moe_kernel_source=cfg.moe_kernel_source, ), ops.MoEDispatch( f"{prefix}_moe_post_dispatch", diff --git a/python/aisimulate/src/aisimulate_core/sdk/models/deepseek.py b/python/aisimulate/src/aisimulate_core/sdk/models/deepseek.py index c519847637..160c9bcfb2 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/models/deepseek.py +++ b/python/aisimulate/src/aisimulate_core/sdk/models/deepseek.py @@ -799,6 +799,7 @@ def _attn_mode(group: str) -> common.GEMMQuantMode: moe_quant_mode, workload_distribution, attention_dp_size, + moe_kernel_source=self.config.moe_kernel_source, ) ] ) @@ -1029,6 +1030,7 @@ def _attn_mode(group: str) -> common.GEMMQuantMode: moe_quant_mode, workload_distribution, attention_dp_size, + moe_kernel_source=self.config.moe_kernel_source, ), ops.MoEDispatch( "generation_moe_post_dispatch", diff --git a/python/aisimulate/src/aisimulate_core/sdk/models/deepseek_v32.py b/python/aisimulate/src/aisimulate_core/sdk/models/deepseek_v32.py index 2bbb1ddcbe..9e076fac70 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/models/deepseek_v32.py +++ b/python/aisimulate/src/aisimulate_core/sdk/models/deepseek_v32.py @@ -502,6 +502,7 @@ def __init__(self, topk: int, num_experts: int, moe_inter_size: int, *args, back moe_quant_mode, workload_distribution, attention_dp_size, + moe_kernel_source=self.config.moe_kernel_source, ), ops.MoEDispatch( "context_moe_post_dispatch", @@ -631,6 +632,7 @@ def __init__(self, topk: int, num_experts: int, moe_inter_size: int, *args, back moe_quant_mode, workload_distribution, attention_dp_size, + moe_kernel_source=self.config.moe_kernel_source, ), ops.MoEDispatch( "generation_moe_post_dispatch", diff --git a/python/aisimulate/src/aisimulate_core/sdk/models/deepseek_v4.py b/python/aisimulate/src/aisimulate_core/sdk/models/deepseek_v4.py index f7e59296d0..d82dad1315 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/models/deepseek_v4.py +++ b/python/aisimulate/src/aisimulate_core/sdk/models/deepseek_v4.py @@ -203,6 +203,7 @@ def _moe_ops(phase: str, num_layers: float, is_context: bool, attn_cp: int = 1): moe_quant_mode, workload_distribution, attention_dp_size, + moe_kernel_source=self.config.moe_kernel_source, ), ops.MoEDispatch( f"{phase}_moe_post_dispatch", diff --git a/python/aisimulate/src/aisimulate_core/sdk/models/deepseek_v41.py b/python/aisimulate/src/aisimulate_core/sdk/models/deepseek_v41.py index eeb1664ca3..2d997f334a 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/models/deepseek_v41.py +++ b/python/aisimulate/src/aisimulate_core/sdk/models/deepseek_v41.py @@ -206,6 +206,7 @@ def stage(layer: int, context: bool): model_config.moe_quant_mode, distribution, model_config.attention_dp_size, + moe_kernel_source=model_config.moe_kernel_source, ), ] # The post-expert reduction consumes both routed and shared diff --git a/python/aisimulate/src/aisimulate_core/sdk/models/gemma4.py b/python/aisimulate/src/aisimulate_core/sdk/models/gemma4.py index c6feb7d7be..14460401d9 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/models/gemma4.py +++ b/python/aisimulate/src/aisimulate_core/sdk/models/gemma4.py @@ -285,6 +285,7 @@ def _moe_ops( moe_q, wl_dist, attn_dp, + moe_kernel_source=self.config.moe_kernel_source, ), ops.MoEDispatch( f"{prefix}_moe_post_dispatch", diff --git a/python/aisimulate/src/aisimulate_core/sdk/models/kimi_k3.py b/python/aisimulate/src/aisimulate_core/sdk/models/kimi_k3.py index 3d37b3c3b3..8305a48e9b 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/models/kimi_k3.py +++ b/python/aisimulate/src/aisimulate_core/sdk/models/kimi_k3.py @@ -723,6 +723,7 @@ def _ffn_ops(self, prefix: str, context: bool) -> list: moe_q, workload_dist, attn_dp, + moe_kernel_source=self.config.moe_kernel_source, ), ops.MoEDispatch( f"{prefix}_moe_post_dispatch", diff --git a/python/aisimulate/src/aisimulate_core/sdk/models/nemotron_h.py b/python/aisimulate/src/aisimulate_core/sdk/models/nemotron_h.py index 9b95049725..3859d7e2cc 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/models/nemotron_h.py +++ b/python/aisimulate/src/aisimulate_core/sdk/models/nemotron_h.py @@ -305,6 +305,7 @@ def _build_context_ops(self) -> None: moe_quant_mode, workload_distribution, attention_dp_size, + moe_kernel_source=self.config.moe_kernel_source, is_gated=False, # NemotronH uses Relu2 (non-gated) ), ops.MoEDispatch( @@ -597,6 +598,7 @@ def _build_generation_ops(self) -> None: moe_quant_mode, workload_distribution, attention_dp_size, + moe_kernel_source=self.config.moe_kernel_source, is_gated=False, # NemotronH uses Relu2 (non-gated) ), ops.MoEDispatch( diff --git a/python/aisimulate/src/aisimulate_core/sdk/models/qwen35.py b/python/aisimulate/src/aisimulate_core/sdk/models/qwen35.py index 2fa04a7d25..96b790adb1 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/models/qwen35.py +++ b/python/aisimulate/src/aisimulate_core/sdk/models/qwen35.py @@ -580,6 +580,7 @@ def _ffn_context_ops( attn_dp, is_context=True, moe_backend=self.config.moe_backend, + moe_kernel_source=self.config.moe_kernel_source, # EPLB is not modeled for Qwen3.5 (the load curve stays 1.2). enable_eplb=False, ) @@ -865,6 +866,7 @@ def _ffn_generation_ops( attn_dp, is_context=False, moe_backend=self.config.moe_backend, + moe_kernel_source=self.config.moe_kernel_source, enable_eplb=False, ) ) diff --git a/python/aisimulate/src/aisimulate_core/sdk/rust_engine_step.py b/python/aisimulate/src/aisimulate_core/sdk/rust_engine_step.py index d0426533fd..6934d370bc 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/rust_engine_step.py +++ b/python/aisimulate/src/aisimulate_core/sdk/rust_engine_step.py @@ -129,6 +129,7 @@ class ForwardPassPerfModelConfig: fallback_policy: str = "deny" estimator_config: dict[str, Any] = dataclass_field(default_factory=dict) attention_backend: str | None = None + moe_kernel_source: str | None = None enable_shared_layer: bool | None = None strict_provenance: bool = False moe_backend: str | None = None @@ -1195,6 +1196,7 @@ def _engine_config_json(model: Any, database: Any) -> str: # per phase); without this key they would share a cached handle and # silently answer with the other mode's engine. "forward_model": getattr(model, "forward_model", "op_level"), + "moe_kernel_source": getattr(model_config, "moe_kernel_source", None), # Same identity built against different systems roots reads different # perf trees; the root is part of the engine identity. "systems_root": str(getattr(database, "systems_root", "") or ""), @@ -1233,6 +1235,7 @@ def _engine_config_json(model: Any, database: Any) -> str: "sms": getattr(model_config, "sms", None), "moe_backend": getattr(model_config, "moe_backend", None), "attention_backend": getattr(model_config, "attention_backend", None), + "moe_kernel_source": getattr(model_config, "moe_kernel_source", None), # enable_wideep is gone from the identity: the deprecated # flag is constant False on every Task-built ModelConfig; # moe_comm_backend + num_gpus_per_node below carry the diff --git a/python/aisimulate/src/aisimulate_core/sdk/speculation/dspark.py b/python/aisimulate/src/aisimulate_core/sdk/speculation/dspark.py index ce57eb808e..78bdb9a2b0 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/speculation/dspark.py +++ b/python/aisimulate/src/aisimulate_core/sdk/speculation/dspark.py @@ -286,6 +286,7 @@ def _v4_block_ops(self, model, *, is_context: bool) -> list: cfg.moe_quant_mode, workload_distribution, cfg.attention_dp_size, + moe_kernel_source=cfg.moe_kernel_source, ), ops.MoEDispatch( f"dspark_{phase}_moe_post_dispatch", diff --git a/python/aisimulate/tests/cross_package/test_core_public_api.py b/python/aisimulate/tests/cross_package/test_core_public_api.py index 0ec0385a28..52e8404209 100644 --- a/python/aisimulate/tests/cross_package/test_core_public_api.py +++ b/python/aisimulate/tests/cross_package/test_core_public_api.py @@ -95,6 +95,7 @@ def test_stable_function_signatures() -> None: "gemm_quant_mode: 'str | None' = None, moe_quant_mode: 'str | None' = None, " "kvcache_quant_mode: 'str | None' = None, fmha_quant_mode: 'str | None' = None, " "comm_quant_mode: 'str | None' = None, attention_backend: 'str | None' = None, " + "moe_kernel_source: 'str | None' = None, " "moe_backend: 'str | None' = None, enable_eplb: 'bool' = False, wideep_num_slots: 'int | None' = None, " "nextn: 'int' = 0, " "speculation: 'dict | None' = None, " diff --git a/python/aisimulate/tests/unit/sdk/database/test_attention_lanes.py b/python/aisimulate/tests/unit/sdk/database/test_attention_lanes.py index be3bc872b8..b1cd427c09 100644 --- a/python/aisimulate/tests/unit/sdk/database/test_attention_lanes.py +++ b/python/aisimulate/tests/unit/sdk/database/test_attention_lanes.py @@ -243,11 +243,11 @@ def _fetch(database, attribute, *, shared_layer=None): monkeypatch.setattr(_etv, "fetch_attention_lane_density", _fetch) -def test_engine_spec_schema_version_is_eighteen(): - """Verification width fields extend the positional operation payload.""" +def test_engine_spec_schema_version_is_twenty(): + """Exact MoE kernel-source selection extends the positional operation payload.""" from aisimulate.sdk import engine - assert engine.ENGINE_SPEC_SCHEMA_VERSION == 19 + assert engine.ENGINE_SPEC_SCHEMA_VERSION == 20 def test_lanes_outside_the_known_vocabulary_stay_reachable(): diff --git a/python/aisimulate/tests/unit/sdk/models/test_deepseek_v41.py b/python/aisimulate/tests/unit/sdk/models/test_deepseek_v41.py index 2307f09c12..2a19fb8f80 100644 --- a/python/aisimulate/tests/unit/sdk/models/test_deepseek_v41.py +++ b/python/aisimulate/tests/unit/sdk/models/test_deepseek_v41.py @@ -138,6 +138,36 @@ def _build_model(*, replay=False, backend="sglang", tp=4): ) +@pytest.mark.parametrize("replay", [False, True]) +@pytest.mark.parametrize("source", [None, "sglang_flashinfer_trtllm_moe"]) +def test_moe_kernel_source_reaches_every_stage_child(replay, source): + import json + + from aisimulate_core.sdk.config import ModelConfig + from aisimulate_core.sdk.models import get_model + + model = get_model( + MODEL_PATH, + ModelConfig(tp_size=4, moe_tp_size=1, moe_ep_size=4, decoder_replay=replay, moe_kernel_source=source), + "sglang", + ) + + def moe_specs(value): + if isinstance(value, dict): + if "Moe" in value: + yield value["Moe"] + for child in value.values(): + yield from moe_specs(child) + elif isinstance(value, list): + for child in value: + yield from moe_specs(child) + + for phase in (model.context_ops, model.generation_ops): + moes = [moe for op in phase for moe in moe_specs(json.loads(op._spec_json()))] + assert len(moes) == 40 + assert all(moe.get("moe_kernel_source") == source for moe in moes) + + @pytest.mark.parametrize("backend", ["sglang", "vllm", "trtllm"]) def test_v41_text_graph_full_profile_all_backends(backend): import json diff --git a/python/aisimulate/tests/unit/sdk/models/test_moe_block_builder_followups.py b/python/aisimulate/tests/unit/sdk/models/test_moe_block_builder_followups.py index 81161bf0d6..d0aefc1bc8 100644 --- a/python/aisimulate/tests/unit/sdk/models/test_moe_block_builder_followups.py +++ b/python/aisimulate/tests/unit/sdk/models/test_moe_block_builder_followups.py @@ -59,6 +59,11 @@ def _dispatches(op_list): class TestGpusPerNodeGuard: + def test_large_ep_rejects_an_explicit_moe_kernel_source(self): + cfg = _cfg(moe_comm_backend={"context": "deepep_ht"}, moe_kernel_source="pinned_source") + with pytest.raises(ValueError, match="moe_kernel_source.*large-EP"): + _build(cfg, gpus_per_node=8) + def test_large_ep_without_gpus_per_node_raises(self): cfg = _cfg(moe_comm_backend={"context": "deepep_ht"}) with pytest.raises(ValueError, match="gpus_per_node"): diff --git a/python/aisimulate/tests/unit/sdk/task_v2/test_task_config.py b/python/aisimulate/tests/unit/sdk/task_v2/test_task_config.py index 3dc039e4c0..3de50b7527 100644 --- a/python/aisimulate/tests/unit/sdk/task_v2/test_task_config.py +++ b/python/aisimulate/tests/unit/sdk/task_v2/test_task_config.py @@ -405,6 +405,82 @@ def test_attention_backend_and_wideep_num_slots_reach_model_config(): assert mc.wideep_num_slots == 288 +def test_moe_kernel_source_reaches_qwen38_compute_ops_and_compiled_spec(): + """A V2 Task pins only Qwen3.8's compute MoE lane, including the native spec.""" + from aisimulate.sdk import models + from aisimulate.sdk.engine import build_engine_spec_json + from aisimulate.sdk.operations.moe import MoE, MoEDispatch + from aisimulate.sdk.perf_database import get_database + + lane = "sglang_flashinfer_trtllm_moe" + task = Task( + serving_mode="agg", + model_path="Qwen/Qwen3.8-2.4T-A95B-FP8", + system_name="b200_sxm", + backend_name="sglang", + backend_version="0.5.17", + moe_kernel_source=lane, + ) + model_config = task.build_model_config(role="agg", parallel=(8, 1, 1, 1, 8, 1)) + assert model_config.moe_kernel_source == lane + + model = models.get_model(task.model_path, model_config, task.backend_name) + + def all_ops(ops): + pending = list(ops) + while pending: + op = pending.pop() + yield op + for group in ("_group_a", "_group_b", "_fallback"): + pending.extend(getattr(op, group, ()) or ()) + + ops = list(all_ops([*model.context_ops, *model.generation_ops])) + compute_ops = [op for op in ops if isinstance(op, MoE)] + dispatch_ops = [op for op in ops if isinstance(op, MoEDispatch)] + assert compute_ops + assert all(op._moe_kernel_source == lane for op in compute_ops) + assert all(not hasattr(op, "_moe_kernel_source") for op in dispatch_ops) + + database = get_database("b200_sxm", "sglang", "0.5.17") + spec = json.loads( + build_engine_spec_json( + model, + model_path=task.model_path, + system=task.system_name, + backend=task.backend_name, + backend_version=task.backend_version, + kv_block_size=None, + systems_path=None, + nextn=task.nextn, + database=database, + ) + ) + + def all_moe_specs(value): + if isinstance(value, list): + for item in value: + yield from all_moe_specs(item) + elif isinstance(value, dict): + if "Moe" in value: + yield value["Moe"] + for item in value.values(): + yield from all_moe_specs(item) + + moe_specs = list(all_moe_specs([spec["context_ops"], spec["generation_ops"]])) + assert spec["engine"]["moe_kernel_source"] == lane + assert moe_specs + assert all(moe["moe_kernel_source"] == lane for moe in moe_specs) + + default_config = Task( + serving_mode="agg", + model_path="Qwen/Qwen3.8-2.4T-A95B-FP8", + system_name="b200_sxm", + backend_name="sglang", + backend_version="0.5.17", + ).build_model_config(role="agg") + assert default_config.moe_kernel_source is None + + def test_invalid_attention_backend_rejected(): with pytest.raises(ValueError, match="attention_backend"): Task( diff --git a/python/aisimulate/tests/unit/sdk/task_v2/test_v1_compat.py b/python/aisimulate/tests/unit/sdk/task_v2/test_v1_compat.py index 39e82d921d..799981a4fd 100644 --- a/python/aisimulate/tests/unit/sdk/task_v2/test_v1_compat.py +++ b/python/aisimulate/tests/unit/sdk/task_v2/test_v1_compat.py @@ -227,6 +227,17 @@ def test_attention_backend_and_wideep_num_slots_map(self): assert out["attention_backend"] == "fa3" assert out["wideep_num_slots"] == 288 + def test_moe_kernel_source_maps_from_legacy_config(self): + """The recipe-pinned MoE lane remains explicit after V1 conversion.""" + out = convert_v1_to_v2( + { + "serving_mode": "agg", + "model_path": "Qwen/Qwen3.8-2.4T-A95B-FP8", + "config": {"moe_kernel_source": "sglang_flashinfer_trtllm_moe"}, + } + ) + assert out["moe_kernel_source"] == "sglang_flashinfer_trtllm_moe" + def test_multiple_profiles_raise(self): """Multiple V1 profiles drop all but the first -- a silent semantic change, so reject.""" with pytest.raises(ValueError) as exc: diff --git a/python/aisimulate/tests/unit/sdk/test_compile_engine_mtp.py b/python/aisimulate/tests/unit/sdk/test_compile_engine_mtp.py index 2492adf964..7817421de6 100644 --- a/python/aisimulate/tests/unit/sdk/test_compile_engine_mtp.py +++ b/python/aisimulate/tests/unit/sdk/test_compile_engine_mtp.py @@ -112,6 +112,27 @@ def _capture_spec(model, **_kwargs): assert captured["model"].config.attention_backend == "fa3" +def test_compile_engine_propagates_moe_kernel_source_to_model_config(monkeypatch): + captured = {} + + def _capture_spec(model, **_kwargs): + captured["model"] = model + return "{}" + + monkeypatch.setattr(engine, "build_engine_spec_json", _capture_spec) + monkeypatch.setattr(engine, "_maybe_load_database", lambda *a, **k: None) + monkeypatch.setattr(engine.aisimulate_core, "engine_spec_bincode_from_json", lambda s: b"") + + engine.compile_engine( + "Qwen/Qwen3-32B", + "h200_sxm", + "trtllm", + moe_kernel_source="sglang_flashinfer_trtllm_moe", + ) + + assert captured["model"].config.moe_kernel_source == "sglang_flashinfer_trtllm_moe" + + def test_compile_engine_propagates_database_mode_to_database_view(monkeypatch): captured = {} diff --git a/python/aisimulate/tests/unit/sdk/test_moe_kernel_source_consumers.py b/python/aisimulate/tests/unit/sdk/test_moe_kernel_source_consumers.py new file mode 100644 index 0000000000..0f77315a3c --- /dev/null +++ b/python/aisimulate/tests/unit/sdk/test_moe_kernel_source_consumers.py @@ -0,0 +1,148 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Exact MoE source survives current CLI, Sweeper and Replay boundaries.""" + +from dataclasses import replace +from types import SimpleNamespace + +import pytest + +from aisimulate.compiler import _worker_engine_args +from aisimulate.config.engine import EnginePredictionConfig +from aisimulate.runner import _materialize_engine_role +from aisimulate.sweeper.config import SearchSpace +from aisimulate.sweeper.forward_pass_estimator import ForwardPassEstimatorResolver +from aisimulate_core.sdk import RustForwardPassPerfModel + +pytestmark = pytest.mark.unit +SOURCE = "sglang_flashinfer_trtllm_moe" + + +@pytest.mark.parametrize("source", [None, SOURCE]) +def test_cli_saved_config_and_replay_preserve_source(source): + engine = EnginePredictionConfig( + model="Qwen/Qwen3-30B-A3B", + hardware="b200_sxm", + backend="sglang", + backend_version="0.5.17", + moe_kernel_source=source, + workers={"aggregated": {"kv_cache": {"capacity": {"type": "fixed", "blocks": 100}}}}, + ) + restored = EnginePredictionConfig.model_validate_json(engine.model_dump_json()) + assert restored.moe_kernel_source == source + payload = _worker_engine_args(restored, restored.workers.aggregated, "aggregated", transfer_bytes_per_token=None) + config = payload["timing_model"]["config"] + assert config.get("moe_kernel_source") == source + assert ("moe_kernel_source" in config) == (source is not None) + role = _materialize_engine_role("sglang", "0.5.17", {}, payload, "aggregated") + assert role["rank"]["timing_model"]["config"].get("moe_kernel_source") == source + + +@pytest.mark.parametrize("field", ["moe_kernel_source", "aic_moe_kernel_source"]) +def test_flat_replay_source_reaches_timing(field): + role = _materialize_engine_role( + "sglang", + "0.5.17", + {}, + {"aic_model_path": "Qwen/Qwen3-30B-A3B", "aic_system": "b200_sxm", "num_gpu_blocks": 100, field: SOURCE}, + "aggregated", + ) + assert role["rank"]["timing_model"]["config"]["moe_kernel_source"] == SOURCE + assert field not in role["rank"] + + +def test_sweeper_source_is_round_tripped_resolved_and_cached(monkeypatch): + space = SearchSpace( + model_name="Qwen/Qwen3-30B-A3B", hardware_sku="b200_sxm", backend=["sglang"], moe_kernel_source=SOURCE + ) + space = SearchSpace.model_validate_json(space.model_dump_json()) + resolver = ForwardPassEstimatorResolver(space) + sample = { + "backend": "sglang", + "deployment_mode": "agg", + "hardware_sku": "b200_sxm", + "tp": 1, + "pp": 1, + "attention_dp": 1, + "moe_tp": 1, + "moe_ep": 1, + "agg_block_size": 1, + } + calls = [] + + def best_available(request): + calls.append(request) + resolved = request.to_dict() | {"backend_version": "0.5.17"} + return SimpleNamespace( + diagnostics=lambda: {"readiness": "ready", "provenance": {"config": resolved}}, close=lambda: None + ) + + monkeypatch.setattr(RustForwardPassPerfModel, "best_available", best_available) + request = resolver._request(sample, "agg") + assert request.moe_kernel_source == SOURCE + assert resolver.resolve_candidate(sample)["agg"].config["moe_kernel_source"] == SOURCE + resolver.resolve_candidate(sample) + assert len(calls) == 1 + resolver._resolve(replace(request, moe_kernel_source=None), "agg") + assert len(calls) == 2 + + +def test_source_rejects_custom_timing_in_current_config(): + with pytest.raises(ValueError, match="engine model controls require default timing"): + EnginePredictionConfig( + model="Qwen/Qwen3-30B-A3B", + hardware="b200_sxm", + moe_kernel_source=SOURCE, + workers={"aggregated": {"timing": {"type": "fixed", "prefill_ms": 1, "decode_ms": 1}}}, + ) + + +def test_sweeper_capacity_preserves_source_through_model_config(monkeypatch): + from aisimulate.sweeper import kv_estimate + from aisimulate.sweeper.parallel_enum import ParallelShape + from aisimulate.sweeper.search_space import _engine_memory_kwargs + from aisimulate_core.sdk import memory + + space = SearchSpace(model_name="Qwen/Qwen3-30B-A3B", hardware_sku="b200_sxm", moe_kernel_source=SOURCE) + captured = {} + original = memory.build_model_config + + def build_config(**kwargs): + captured["config"] = original(**kwargs) + raise RuntimeError("stop before database access") + + monkeypatch.setattr(memory, "build_model_config", build_config) + with pytest.raises(ValueError, match="stop before database access"): + kv_estimate.estimate_kv_tokens( + ParallelShape(1, 1, 1, 1), + model_name=space.model_name, + hardware_sku=space.hardware_sku, + backend="sglang", + backend_version="0.5.17", + **_engine_memory_kwargs(space), + ) + assert captured["config"].moe_kernel_source == SOURCE + + +def test_explicit_source_prevents_naive_capacity_fallback(monkeypatch): + from aisimulate_core.sdk import memory + + def unavailable(*args, **kwargs): + assert kwargs["moe_kernel_source"] == SOURCE + raise RuntimeError("unavailable model") + + monkeypatch.setattr(memory.KVCacheEstimator, "from_request", unavailable) + with pytest.raises(ValueError, match="unavailable model"): + memory.estimate_num_gpu_blocks( + "model", + "b200_sxm", + "sglang", + scheduler_block_size=1, + max_num_tokens=1, + max_batch_size=1, + memory_fraction_kind="of_total", + memory_fraction_value=0.9, + moe_kernel_source=SOURCE, + allow_naive_fallback=True, + ) diff --git a/python/aisimulate/tests/unit/sdk/test_rust_engine_step.py b/python/aisimulate/tests/unit/sdk/test_rust_engine_step.py index 66a1223d22..93ed0588dd 100644 --- a/python/aisimulate/tests/unit/sdk/test_rust_engine_step.py +++ b/python/aisimulate/tests/unit/sdk/test_rust_engine_step.py @@ -935,6 +935,16 @@ def test_forward_pass_config_requires_role_and_defaults_to_auto_deny() -> None: assert config.estimation_mode == "auto" assert config.fallback_policy == "deny" assert config.estimator_config == {} + assert config.moe_kernel_source is None + + pinned = ForwardPassPerfModelConfig( + model="m", + system="s", + backend="vllm", + worker_type="decode", + moe_kernel_source="sglang_flashinfer_trtllm_moe", + ) + assert pinned.to_dict()["moe_kernel_source"] == "sglang_flashinfer_trtllm_moe" def _supported_fpm_config() -> dict[str, object]: @@ -1343,14 +1353,14 @@ def test_sparse_cp_ops_emit_cp_fields_in_spec(): assert spec["attn_kind"] == "Csa" -def test_engine_config_json_identity_disambiguates_collapsed_quant_modes(): +def test_engine_config_json_identity_disambiguates_collapsed_quant_modes_and_moe_lanes(): """Two models differing only in a wire-collapsed dtype (sq vs int8_wo both -> "int8") or an identity-omitted ModelConfig field (moe_backend) must get DISTINCT handle-cache keys — sharing one cached handle silently returns the other model's latencies.""" from aisimulate.sdk import common - def _model(gemm_mode, moe_backend=None): + def _model(gemm_mode, moe_backend=None, moe_kernel_source=None): cfg = SimpleNamespace( tp_size=8, pp_size=1, @@ -1365,6 +1375,7 @@ def _model(gemm_mode, moe_backend=None): comm_quant_mode=None, moe_backend=moe_backend, attention_backend=None, + moe_kernel_source=moe_kernel_source, # enable_wideep dropped from the fixture: the deprecated flag left # the engine identity (constant False; moe_comm_backend + # num_gpus_per_node carry the regime). @@ -1387,6 +1398,11 @@ def _model(gemm_mode, moe_backend=None): ) assert key_sq != key_deepep, "moe_backend must participate in the cache identity" + key_flashinfer = rust_engine_step._engine_config_json( + _model(common.GEMMQuantMode.sq, moe_kernel_source="sglang_flashinfer_trtllm_moe"), database + ) + assert key_sq != key_flashinfer, "a pinned MoE lane must not reuse the default handle" + def test_engine_config_json_identity_includes_database_policy(): """Two views of the SAME on-disk identity that differ only in the From 5a9c3b30c55640f62350800d02535e553ed43cfd Mon Sep 17 00:00:00 2001 From: Yiming Liu Date: Mon, 21 Sep 2026 17:18:37 +0800 Subject: [PATCH 02/14] fix: enforce exact low-latency MoE eligibility Signed-off-by: Yiming Liu --- crates/core/src/perfmodel/operators/moe.rs | 174 ++++++++++++++++++++- 1 file changed, 173 insertions(+), 1 deletion(-) diff --git a/crates/core/src/perfmodel/operators/moe.rs b/crates/core/src/perfmodel/operators/moe.rs index 2c870793ea..89a68c8385 100644 --- a/crates/core/src/perfmodel/operators/moe.rs +++ b/crates/core/src/perfmodel/operators/moe.rs @@ -210,7 +210,7 @@ pub struct MoeOp { pub moe_backend: Option, /// Exact collected MoE kernel-source lane. `None` preserves the /// framework/default lookup; `Some` selects only rows carrying this exact - /// `kernel_source` value and fails on a missing lane. + /// `kernel_source` value and fails on a missing or ineligible lane. #[serde(default)] pub moe_kernel_source: Option, /// EPLB enabled (Python `MoE._enable_eplb`). On the sglang branch the @@ -317,6 +317,21 @@ impl MoeOp { } } + fn validate_kernel_source(&self, num_tokens: u32) -> Result<(), AicError> { + if self.moe_kernel_source.as_deref() == Some("moe_torch_flow_min_latency") + && (!self.is_gated || self.quant_mode != MoeQuantMode::Nvfp4 || num_tokens > 128) + { + return Err(AicError::InvalidEngineConfig(format!( + "MoE kernel source moe_torch_flow_min_latency requires gated nvfp4 with at most \ + 128 tokens; op {} has is_gated={}, quant={}, num_tokens={num_tokens}", + self.name, + self.is_gated, + self.quant_mode.name(), + ))); + } + Ok(()) + } + /// SILICON resolution (retired-deepep gate + low-latency probe + the default /// grid, scale/clamp applied per branch — the audit-PR body, unchanged). pub(crate) fn silicon_pr( @@ -324,6 +339,7 @@ impl MoeOp { db: &PerfDatabase, num_tokens: u32, ) -> Result { + self.validate_kernel_source(num_tokens)?; let is_sglang = db.backend == "sglang"; // SGLang EPLB prefill correction — INSIDE get_silicon only (Python // operations/moe.py:684, sglang branch: `num_tokens_corrected = @@ -437,6 +453,7 @@ impl MoeOp { /// `MoE._query_moe_table::get_empirical` (`operations/moe.py:327-572`): /// own-shape grid → xshape → xquant → xprofile → typed empirical miss. fn empirical_latency(&self, db: &PerfDatabase, num_tokens: u32) -> Result { + self.validate_kernel_source(num_tokens)?; let spec = &db.system_spec; let quant = self.quant_mode; let num_gemms: u64 = if self.is_gated { 3 } else { 2 }; @@ -1008,6 +1025,161 @@ mod tests { assert_eq!(result.source, source, "{label}: wrong source"); } + fn exact_source_fixture(mode: DatabaseMode) -> (tempfile::TempDir, PerfDatabase, MoeOp) { + use crate::perf_database::energy_test_fixtures::{ + Col, write_energy_systems_root, write_parquet, + }; + + // Synthetic values distinguish the restricted NVFP4 lane from the + // unrestricted FlashInfer FP8 lane; these are not measured timings. + let tmp = tempfile::tempdir().expect("tmpdir"); + let data = write_energy_systems_root(tmp.path()); + write_parquet( + &data.join("moe_perf.parquet"), + &[ + Col::Str( + "moe_dtype", + vec!["nvfp4", "nvfp4", "fp8_block", "fp8_block"], + ), + Col::I64("num_tokens", vec![64, 128, 64, 256]), + Col::I64("hidden_size", vec![4096; 4]), + Col::I64("inter_size", vec![2048; 4]), + Col::I64("topk", vec![2; 4]), + Col::I64("num_experts", vec![8; 4]), + Col::I64("moe_tp_size", vec![1; 4]), + Col::I64("moe_ep_size", vec![1; 4]), + Col::Str("distribution", vec!["uniform"; 4]), + Col::Str( + "kernel_source", + vec![ + "moe_torch_flow_min_latency", + "moe_torch_flow_min_latency", + "sglang_flashinfer_trtllm_moe", + "sglang_flashinfer_trtllm_moe", + ], + ), + Col::F64("latency", vec![1.0, 2.0, 3.0, 6.0]), + ], + ); + let db = PerfDatabase::load(tmp.path(), "testsys", "vllm", "1.0") + .expect("synthetic database") + .with_mode(mode, TransferPolicy::ALL); + let mut op = MoeOp::new( + "moe", + 4096, + 2048, + 2, + 8, + 1, + 1, + MoeQuantMode::Nvfp4, + "uniform", + ); + op.moe_kernel_source = Some("moe_torch_flow_min_latency".into()); + (tmp, db, op) + } + + fn assert_explicit_low_latency_rejects_ineligible_queries(mode: DatabaseMode) { + let (_tmp, db, op) = exact_source_fixture(mode); + for (is_gated, quant, tokens, attention_dp_size) in [ + (false, MoeQuantMode::Nvfp4, 64, 1), + (true, MoeQuantMode::Nvfp4, 129, 1), + (true, MoeQuantMode::Fp8Block, 64, 1), + (true, MoeQuantMode::Nvfp4Wo, 64, 1), + (true, MoeQuantMode::Nvfp4, 65, 2), + ] { + let mut invalid = op.clone(); + invalid.is_gated = is_gated; + invalid.quant_mode = quant; + invalid.attention_dp_size = attention_dp_size; + let result = invalid.query(&db, tokens); + assert!( + matches!(&result, Err(AicError::InvalidEngineConfig(message)) + if message.contains("moe_torch_flow_min_latency") + && message.contains("gated nvfp4") + && message.contains("128")), + "ineligible exact lane must fail before lookup or transfer: {mode:?}, \ + gated={is_gated}, quant={quant:?}, tokens={tokens}, \ + attention_dp={attention_dp_size}, got {result:?}" + ); + } + } + + #[test] + fn moe_explicit_low_latency_rejects_ineligible_silicon_queries() { + assert_explicit_low_latency_rejects_ineligible_queries(DatabaseMode::Silicon); + } + + #[test] + fn moe_explicit_low_latency_rejects_ineligible_empirical_queries() { + assert_explicit_low_latency_rejects_ineligible_queries(DatabaseMode::Empirical); + } + + #[test] + fn moe_explicit_low_latency_rejects_ineligible_hybrid_queries() { + assert_explicit_low_latency_rejects_ineligible_queries(DatabaseMode::Hybrid); + } + + #[test] + fn moe_explicit_low_latency_accepts_gated_nvfp4_through_128_tokens() { + for mode in [ + DatabaseMode::Silicon, + DatabaseMode::Empirical, + DatabaseMode::Hybrid, + ] { + let (_tmp, db, mut op) = exact_source_fixture(mode); + let source = if mode == DatabaseMode::Empirical { + Source::Empirical + } else { + Source::Silicon + }; + // Endpoints are the synthetic rows. Linear interpolation at 127 + // is 1 + 63/64 of the 1 ms interval: 1.984375 ms. + for (tokens, expected_ms) in [(64, 1.0), (127, 1.984375), (128, 2.0)] { + let result = op.query(&db, tokens).expect("eligible exact lane"); + assert_routing(&result, source, "low latency"); + if source == Source::Silicon { + assert_eq!(result.latency_ms, expected_ms); + } + } + op.attention_dp_size = 2; + let result = op.query(&db, 64).expect("128 gathered tokens"); + assert_routing(&result, source, "low latency with dp"); + if source == Source::Silicon { + assert_eq!(result.latency_ms, 2.0); + } + } + } + + #[test] + fn moe_other_exact_sources_allow_non_gated_fp8_above_128_tokens() { + for mode in [ + DatabaseMode::Silicon, + DatabaseMode::Empirical, + DatabaseMode::Hybrid, + ] { + let (_tmp, db, mut op) = exact_source_fixture(mode); + op.moe_kernel_source = Some("sglang_flashinfer_trtllm_moe".into()); + op.is_gated = false; + op.quant_mode = MoeQuantMode::Fp8Block; + let result = op + .query(&db, 256) + .expect("other source is not low-latency restricted"); + assert_routing( + &result, + if mode == DatabaseMode::Empirical { + Source::Empirical + } else { + Source::Silicon + }, + "other exact source", + ); + if result.source == Source::Silicon { + assert_eq!(result.latency_ms, 6.0); // exact synthetic FlashInfer row + } + } + } + /// Oracle values generated from the Python reference on the same data /// (shared layer pinned OFF so Python reads exactly the primary parquet /// the Rust table loads): From 00d3ffd1d6895378662e30506f90693f1695bdcd Mon Sep 17 00:00:00 2001 From: Yiming Liu Date: Mon, 21 Sep 2026 17:33:13 +0800 Subject: [PATCH 03/14] fix: preserve MoE source configuration compatibility Signed-off-by: Yiming Liu --- .../core/src/perfmodel/operators/attention.rs | 6 +-- .../core/tests/perfmodel/memory_round_trip.rs | 1 + crates/tests/public-api/src/lib.rs | 4 +- .../src/aisimulate_core/sdk/config.py | 2 +- .../aisimulate_core/sdk/config_builders.py | 3 +- .../unit/sdk/models/test_model_config.py | 54 +++++++++++++++++++ .../unit/sdk/task_v2/test_task_config.py | 16 ++++++ 7 files changed, 80 insertions(+), 6 deletions(-) diff --git a/crates/core/src/perfmodel/operators/attention.rs b/crates/core/src/perfmodel/operators/attention.rs index 1f8d77c554..604e77ace6 100644 --- a/crates/core/src/perfmodel/operators/attention.rs +++ b/crates/core/src/perfmodel/operators/attention.rs @@ -184,7 +184,7 @@ pub struct ContextAttentionOp { /// lanes, density-ranked donor tiers, `"default"`, and the table's own /// leftover lanes — and it is REPLAYED VERBATIM here: no re-deriving, no /// extending, no sorting. Appended at the struct TAIL because bincode - /// payloads are positional (current ENGINE_SPEC_SCHEMA_VERSION 19). + /// payloads are positional (current ENGINE_SPEC_SCHEMA_VERSION 20). #[serde(default = "default_lane_order")] pub lane_order: Vec, /// Whether the fused prefill kernel applies rotary position embeddings. @@ -197,7 +197,7 @@ pub struct ContextAttentionOp { /// Lane precedence for ops built without an explicit order (Rust-side /// constructors and hand-written JSON fixtures predating the `lane_order` -/// field — introduced at schema v8, current ENGINE_SPEC_SCHEMA_VERSION 19). +/// field — introduced at schema v8, current ENGINE_SPEC_SCHEMA_VERSION 20). /// Mirrors the Python fallback in `_attention_lane_order` for an /// unresolvable database: the always-valid `("default",)`. pub(crate) fn default_lane_order() -> Vec { @@ -385,7 +385,7 @@ pub struct GenerationAttentionOp { pub kv_cache_dtype: KvCacheQuantMode, /// Kernel-source lane precedence; see /// [`ContextAttentionOp::lane_order`] (appended at the struct TAIL — - /// bincode payloads are positional, current ENGINE_SPEC_SCHEMA_VERSION 19). + /// bincode payloads are positional, current ENGINE_SPEC_SCHEMA_VERSION 20). #[serde(default = "default_lane_order")] pub lane_order: Vec, /// Per-head RMSNorm on Q and K before decode attention. Appended at the diff --git a/crates/core/tests/perfmodel/memory_round_trip.rs b/crates/core/tests/perfmodel/memory_round_trip.rs index 0af0f5cf88..2d66e3c0c9 100644 --- a/crates/core/tests/perfmodel/memory_round_trip.rs +++ b/crates/core/tests/perfmodel/memory_round_trip.rs @@ -88,6 +88,7 @@ fn request(tolerance_fraction: Option) -> KvCacheEstimateRequest { forward_model: None, fpm_parquet_path: None, decoder_replay: false, + moe_kernel_source: None, kv_block_size: None, parallel: ParallelMapping { tp_size: 1, diff --git a/crates/tests/public-api/src/lib.rs b/crates/tests/public-api/src/lib.rs index c115f5c3d2..3ca806ddbb 100644 --- a/crates/tests/public-api/src/lib.rs +++ b/crates/tests/public-api/src/lib.rs @@ -120,7 +120,9 @@ mod tests { // v17: ContextAttentionOp gained apply_rope (Muse Glimmer review // follow-up) — a positional bincode op-layout change. // v18: speculative attention width fields and FpmForward verify_width. - assert_eq!(ENGINE_SPEC_SCHEMA_VERSION, 19); + // v19: Dsv41AttentionOp gained kv_cache_layout. + // v20: EngineConfig and MoeOp gained exact moe_kernel_source identity. + assert_eq!(ENGINE_SPEC_SCHEMA_VERSION, 20); assert_eq!(FPM_VERSION, 1); assert_eq!(ForwardPassMetrics::default().version, FPM_VERSION); } diff --git a/python/aisimulate/src/aisimulate_core/sdk/config.py b/python/aisimulate/src/aisimulate_core/sdk/config.py index cc59d51e37..9c3f627e51 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/config.py +++ b/python/aisimulate/src/aisimulate_core/sdk/config.py @@ -32,7 +32,7 @@ def normalize_kernel_source(value: str | None, field_name: str) -> str | None: """Validate an optional exact collected kernel-source label.""" if value is None: return None - if not isinstance(value, str) or not value: + if not isinstance(value, str) or not value.strip(): raise ValueError(f"{field_name} must be a non-empty string or None, got {value!r}.") return value diff --git a/python/aisimulate/src/aisimulate_core/sdk/config_builders.py b/python/aisimulate/src/aisimulate_core/sdk/config_builders.py index 9171e2504a..0fbee6803f 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/config_builders.py +++ b/python/aisimulate/src/aisimulate_core/sdk/config_builders.py @@ -39,11 +39,12 @@ def build_model_config( forward_model: str | None = None, enable_encoder_dp: bool = True, attention_backend: str | None = None, - moe_kernel_source: str | None = None, speculation=None, moe_backend: str | None = None, enable_eplb: bool = False, wideep_num_slots: int | None = None, + *, + moe_kernel_source: str | None = None, ) -> ModelConfig: """Build a ModelConfig with optional quant mode overrides.""" validate_moe_controls(enable_eplb=enable_eplb, wideep_num_slots=wideep_num_slots) diff --git a/python/aisimulate/tests/unit/sdk/models/test_model_config.py b/python/aisimulate/tests/unit/sdk/models/test_model_config.py index c86350ddf1..26f0ab36f9 100644 --- a/python/aisimulate/tests/unit/sdk/models/test_model_config.py +++ b/python/aisimulate/tests/unit/sdk/models/test_model_config.py @@ -16,6 +16,7 @@ import aisimulate.sdk.operations as ops from aisimulate.sdk import common, config, models +from aisimulate.sdk.config_builders import build_model_config from aisimulate.sdk.models import ( LLAMAModel, Qwen3VLModel, @@ -25,6 +26,7 @@ get_model_family, ) from aisimulate.sdk.performance_result import PerformanceResult +from aisimulate.sdk.speculation import SpeculationConfig from aisimulate.sdk.utils import get_model_config_from_model_path pytestmark = pytest.mark.unit @@ -49,6 +51,58 @@ def test_model_config_rejects_unknown_kernel_backend(field, value): config.ModelConfig(**{field: value}) +def test_build_model_config_preserves_existing_positional_arguments(): + speculation = SpeculationConfig(kind="mtp", params={"depth": 2}) + model_config = build_model_config( + 4, + 2, + 2, + 2, + 4, + "fp8", + "fp8", + "bfloat16", + "fp8", + "half", + "fpm", + False, + "fa3", + speculation, + "megamoe", + True, + 64, + ) + + assert (model_config.tp_size, model_config.pp_size, model_config.attention_dp_size) == (4, 2, 2) + assert (model_config.moe_tp_size, model_config.moe_ep_size) == (2, 4) + assert model_config.gemm_quant_mode is common.GEMMQuantMode.fp8 + assert model_config.kvcache_quant_mode is common.KVCacheQuantMode.fp8 + assert model_config.fmha_quant_mode is common.FMHAQuantMode.bfloat16 + assert model_config.moe_quant_mode is common.MoEQuantMode.fp8 + assert model_config.comm_quant_mode is common.CommQuantMode.half + assert model_config.forward_model == "fpm" + assert model_config.enable_encoder_dp is False + assert model_config.attention_backend is common.AttentionBackend.fa3 + assert model_config.speculation is speculation + assert model_config.moe_backend is common.MoEBackend.megamoe + assert model_config.enable_eplb is True + assert model_config.wideep_num_slots == 64 + assert model_config.moe_kernel_source is None + + +@pytest.mark.parametrize("source", [None, "sglang_flashinfer_trtllm_moe", " source_with_spaces "]) +def test_build_model_config_preserves_exact_kernel_source_keyword(source): + model_config = build_model_config(4, 1, 1, 4, 1, moe_kernel_source=source) + + assert model_config.moe_kernel_source == source + + +@pytest.mark.parametrize("source", ["", " ", "\t\n", "\u2003", 1, False, []]) +def test_model_config_rejects_invalid_kernel_source(source): + with pytest.raises(ValueError, match="moe_kernel_source must be a non-empty string"): + config.ModelConfig(moe_kernel_source=source) + + class TestSupportedModels: """Test default models configuration from support_matrix.csv.""" diff --git a/python/aisimulate/tests/unit/sdk/task_v2/test_task_config.py b/python/aisimulate/tests/unit/sdk/task_v2/test_task_config.py index 3de50b7527..f5521ac867 100644 --- a/python/aisimulate/tests/unit/sdk/task_v2/test_task_config.py +++ b/python/aisimulate/tests/unit/sdk/task_v2/test_task_config.py @@ -405,6 +405,22 @@ def test_attention_backend_and_wideep_num_slots_reach_model_config(): assert mc.wideep_num_slots == 288 +@pytest.mark.parametrize("source", ["", " ", "\t\n", "\u2003", 1, False, []]) +def test_task_rejects_invalid_moe_kernel_source(source): + with pytest.raises(ValueError, match="moe_kernel_source must be a non-empty string"): + Task(moe_kernel_source=source) + + +@pytest.mark.parametrize("source", [None, "sglang_flashinfer_trtllm_moe", " source_with_spaces "]) +def test_task_round_trip_preserves_exact_moe_kernel_source(source): + task = Task(moe_kernel_source=source) + restored = Task.from_yaml(yaml.safe_load(task.to_yaml())) + + assert task.moe_kernel_source == source + assert restored.moe_kernel_source == source + assert restored.build_model_config(role="agg").moe_kernel_source == source + + def test_moe_kernel_source_reaches_qwen38_compute_ops_and_compiled_spec(): """A V2 Task pins only Qwen3.8's compute MoE lane, including the native spec.""" from aisimulate.sdk import models From 350b26ebec97117cbff2478286f7a9770e3576a5 Mon Sep 17 00:00:00 2001 From: Yiming Liu Date: Mon, 21 Sep 2026 17:40:07 +0800 Subject: [PATCH 04/14] fix: preserve canonical config positional arguments Signed-off-by: Yiming Liu --- .../aisimulate_core/sdk/rust_engine_step.py | 2 +- .../tests/unit/sdk/test_rust_engine_step.py | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/python/aisimulate/src/aisimulate_core/sdk/rust_engine_step.py b/python/aisimulate/src/aisimulate_core/sdk/rust_engine_step.py index 6934d370bc..85d5761c3b 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/rust_engine_step.py +++ b/python/aisimulate/src/aisimulate_core/sdk/rust_engine_step.py @@ -129,7 +129,7 @@ class ForwardPassPerfModelConfig: fallback_policy: str = "deny" estimator_config: dict[str, Any] = dataclass_field(default_factory=dict) attention_backend: str | None = None - moe_kernel_source: str | None = None + moe_kernel_source: str | None = dataclass_field(default=None, kw_only=True) enable_shared_layer: bool | None = None strict_provenance: bool = False moe_backend: str | None = None diff --git a/python/aisimulate/tests/unit/sdk/test_rust_engine_step.py b/python/aisimulate/tests/unit/sdk/test_rust_engine_step.py index 93ed0588dd..af2841e192 100644 --- a/python/aisimulate/tests/unit/sdk/test_rust_engine_step.py +++ b/python/aisimulate/tests/unit/sdk/test_rust_engine_step.py @@ -947,6 +947,52 @@ def test_forward_pass_config_requires_role_and_defaults_to_auto_deny() -> None: assert pinned.to_dict()["moe_kernel_source"] == "sglang_flashinfer_trtllm_moe" +def test_forward_pass_config_preserves_existing_positional_arguments(tmp_path: Path) -> None: + from aisimulate_core.sdk import ForwardPassPerfModelConfig + + # Positional order frozen from main at e8828036e3d3, before moe_kernel_source was added. + legacy_fields = { + "model": "Qwen/Qwen3-30B-A3B", + "system": "b200_sxm", + "backend": "sglang", + "worker_type": "decode", + "backend_version": "0.5.17", + "tp": 4, + "pp": 2, + "attention_dp": 2, + "moe_tp_size": 1, + "moe_ep_size": 4, + "gemm_quant_mode": "fp8", + "moe_quant_mode": "fp8_block", + "fmha_quant_mode": "bfloat16", + "kvcache_quant_mode": "fp8", + "comm_quant_mode": "half", + "nextn": 2, + "speculation": {"kind": "mtp", "params": {"depth": 2}}, + "kv_block_size": 64, + "decoder_replay": True, + "estimation_mode": "fpm_regression", + "database_mode": "EMPIRICAL", + "transfer_policy": ("gemm", "moe"), + "systems_paths": (str(tmp_path),), + "fallback_policy": "allow", + "estimator_config": {"correction": {"enabled": False}}, + "attention_backend": "fa3", + "enable_shared_layer": False, + "strict_provenance": True, + "moe_backend": "megamoe", + "enable_eplb": True, + "wideep_num_slots": 64, + } + config = ForwardPassPerfModelConfig(*legacy_fields.values()) + assert vars(config) == {**legacy_fields, "moe_kernel_source": None} + + source = " source_with_spaces " + pinned = ForwardPassPerfModelConfig(*legacy_fields.values(), moe_kernel_source=source) + assert vars(pinned) == {**legacy_fields, "moe_kernel_source": source} + assert json.loads(json.dumps(pinned.to_dict()))["moe_kernel_source"] == source + + def _supported_fpm_config() -> dict[str, object]: return { "schema_version": 1, From 6f56d9da09a08fb2b7a4244c312f2f072e2d9c25 Mon Sep 17 00:00:00 2001 From: Yiming Liu Date: Mon, 21 Sep 2026 18:29:58 +0800 Subject: [PATCH 05/14] fix: reject ignored MoE source selections Validate exact source selections against model metadata and the constructed native operation graph before unsupported configurations can fall back. Preserve the source through external-FPM AFD validation and explicitly reject legacy or fixed companion paths that cannot consume it. Document the selector contract and cover the actual model, memory, Task, and runner consumers. Signed-off-by: Yiming Liu --- docs/core-api.md | 21 +++--- python/aisimulate/src/aisimulate/runner.py | 5 ++ .../aisimulate_core/sdk/config_builders.py | 20 ++++- .../src/aisimulate_core/sdk/engine.py | 14 ++-- .../src/aisimulate_core/sdk/memory.py | 12 ++- .../aisimulate_core/sdk/models/__init__.py | 31 ++++++++ .../aisimulate_core/sdk/models/blocks/moe.py | 5 +- .../unit/sdk/models/test_model_config.py | 34 +++++++++ .../test_moe_block_builder_followups.py | 3 +- .../tests/unit/sdk/test_compile_engine_mtp.py | 2 +- .../sdk/test_moe_kernel_source_consumers.py | 73 ++++++++++++++++++- tests/sweeper/test_engine_request.py | 1 + tests/test_afd_runner.py | 64 +++++++++++++++- 13 files changed, 257 insertions(+), 28 deletions(-) diff --git a/docs/core-api.md b/docs/core-api.md index 69e10e8cc7..9d7a61746e 100644 --- a/docs/core-api.md +++ b/docs/core-api.md @@ -208,18 +208,15 @@ The parquet identity must match the requested model, hardware, backend version, ### Engine identity controls -The canonical configuration also carries quantization overrides and -`attention_backend`, `moe_backend`, `enable_eplb` (default `false`), and -`wideep_num_slots` (default absent). These controls reach model construction, -KV memory sizing, and replay provenance. EPLB/slots and nondefault MoE backend -selection require an MoE model. Collected FPM interpolation cannot represent -EPLB, slots, or MoE backend overrides; it rejects an explicit incompatible -request and is skipped during automatic selection for those identities. - -Rust callers using exhaustive `ForwardPassPerfModelConfig` literals must add -`moe_backend: None`, `enable_eplb: false`, and `wideep_num_slots: None`. -`ForwardPassPerfModelConfig::new(...)` supplies these defaults. This extends -the canonical configuration introduced by #242. +The canonical configuration also carries quantization overrides and `attention_backend`, `moe_backend`, `moe_kernel_source` (default absent), `enable_eplb` (default `false`), and `wideep_num_slots` (default absent). These controls reach model construction, KV memory sizing, and replay provenance. EPLB/slots and nondefault MoE backend or kernel-source selection require an MoE model. Collected FPM interpolation cannot represent EPLB, slots, MoE backend, or kernel-source overrides; it rejects an explicit incompatible request and is skipped during automatic selection for those identities. + +`moe_kernel_source` selects an exact, nonblank collected `kernel_source` label for fused MoE compute. It is distinct from the existing `moe_backend` graph/backend control; source labels are not backend aliases and are preserved without trimming. `None` keeps the existing default source-selection policy, including eligible low-latency NVFP4 selection. `SILICON` reads only the requested source's table; `EMPIRICAL` derives its estimate from that same source; `HYBRID` may fall back to empirical estimation within that source, but does not substitute a different source. Missing source data remains an error. An explicit `moe_torch_flow_min_latency` requires gated NVFP4 and at most 128 tokens after attention-DP gathering. Pure-roofline `SOL` remains table-independent and does not claim measured support for the requested source. + +An explicit source is rejected for dense graphs, MegaMoE modules, large-EP expert-compute graphs, and any constructed timing phase with no compatible fused MoE operator. It is also incompatible with whole-forward FPM, including the legacy Task `forward_model='fpm'` rewrite. Invalid graph/source combinations fail as invalid configuration rather than triggering estimator fallback. An untrained `fpm_regression` model remains not-ready; retaining a source in its configuration is not evidence of source-specific prediction support. + +AFD regular companions currently reject exact-source requests through their legacy estimator and fixed timing paths. The external-FPM companion forwards the source to canonical validation, which rejects the incompatible FPM request. These controls describe standalone AISimulate behavior, not downstream Dynamo planner integration. + +Rust callers using exhaustive `ForwardPassPerfModelConfig` literals must add `moe_backend: None`, `moe_kernel_source: None`, `enable_eplb: false`, and `wideep_num_slots: None`. Direct `EngineConfig` and `MoeOp` literals likewise require the new `moe_kernel_source` field. `ForwardPassPerfModelConfig::new(...)` supplies its default. This extends the canonical configuration introduced by #242. Rust callers constructing `SyntheticTraceSpec` must also add `cached_prefix_tokens: 0` to preserve existing prefix-sharing behavior. A positive diff --git a/python/aisimulate/src/aisimulate/runner.py b/python/aisimulate/src/aisimulate/runner.py index 8c145d5e97..6412cdc82c 100644 --- a/python/aisimulate/src/aisimulate/runner.py +++ b/python/aisimulate/src/aisimulate/runner.py @@ -188,6 +188,8 @@ def measure(self, spec: ReplaySpec) -> AFDCompanionTiming: timing_model = args.get("timing_model") if isinstance(timing_model, Mapping) and timing_model.get("type") == "fixed": + if any(args.get(alias) is not None for alias in _AIC_TIMING_FIELD_ALIASES["moe_kernel_source"]): + raise ValueError("moe_kernel_source is not supported by fixed AFD companion timing") key = "prefill_ms" if role == "prefill" else "decode_ms" latency = _positive_number(timing_model.get(key), f"{role} timing_model.{key}") return AFDCompanionTiming( @@ -288,6 +290,7 @@ def measure(self, spec: ReplaySpec) -> AFDCompanionTiming: for name in ( "attention_backend", "moe_backend", + "moe_kernel_source", "enable_eplb", "wideep_num_slots", "decoder_replay", @@ -310,6 +313,8 @@ def measure(self, spec: ReplaySpec) -> AFDCompanionTiming: raw = {metric: latency} source = "aisimulate_core.sdk.rust_engine_step.RustForwardPassPerfModel" else: + if timing_overrides.get("moe_kernel_source") is not None: + raise ValueError("moe_kernel_source is not supported by the AFD companion legacy estimator") result = estimator(model_name, hardware, **kwargs) raw = getattr(result, "raw", None) except Exception as exc: diff --git a/python/aisimulate/src/aisimulate_core/sdk/config_builders.py b/python/aisimulate/src/aisimulate_core/sdk/config_builders.py index 0fbee6803f..ffa3e1fd60 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/config_builders.py +++ b/python/aisimulate/src/aisimulate_core/sdk/config_builders.py @@ -21,6 +21,7 @@ MoEQuantMode, ) from aisimulate_core.sdk.config import ModelConfig +from aisimulate_core.sdk.errors import InvalidEngineConfigurationError logger = logging.getLogger(__name__) @@ -47,7 +48,12 @@ def build_model_config( moe_kernel_source: str | None = None, ) -> ModelConfig: """Build a ModelConfig with optional quant mode overrides.""" - validate_moe_controls(enable_eplb=enable_eplb, wideep_num_slots=wideep_num_slots) + validate_moe_controls( + enable_eplb=enable_eplb, + wideep_num_slots=wideep_num_slots, + moe_backend=moe_backend, + moe_kernel_source=moe_kernel_source, + ) return ModelConfig( tp_size=tp_size, pp_size=pp_size, @@ -75,6 +81,7 @@ def validate_moe_controls( enable_eplb: bool = False, wideep_num_slots: int | None = None, moe_backend: str | None = None, + moe_kernel_source: str | None = None, model_path: str | None = None, ) -> None: """Reject invalid MoE identity before model construction or fallback.""" @@ -82,11 +89,18 @@ def validate_moe_controls( raise ValueError("enable_eplb must be a boolean") if wideep_num_slots is not None and (type(wideep_num_slots) is not int or wideep_num_slots <= 0): raise ValueError("wideep_num_slots must be a positive integer") - if model_path is not None and (enable_eplb or wideep_num_slots is not None or moe_backend not in (None, "default")): + if moe_kernel_source is not None and moe_backend == "megamoe": + raise InvalidEngineConfigurationError("moe_kernel_source is not supported by the MegaMoE module") + if model_path is not None and ( + enable_eplb + or wideep_num_slots is not None + or moe_backend not in (None, "default") + or moe_kernel_source is not None + ): from aisimulate_core.sdk.models import check_is_moe if not check_is_moe(model_path): - raise ValueError("EPLB, slots and moe_backend require an MoE model") + raise InvalidEngineConfigurationError("EPLB, slots, moe_backend and moe_kernel_source require an MoE model") def validate_nextn(nextn: int | None) -> int: diff --git a/python/aisimulate/src/aisimulate_core/sdk/engine.py b/python/aisimulate/src/aisimulate_core/sdk/engine.py index 9bd1facb2b..af14e8ce3f 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/engine.py +++ b/python/aisimulate/src/aisimulate_core/sdk/engine.py @@ -44,7 +44,7 @@ from typing import Any import aisimulate_core -from aisimulate_core.sdk.config_builders import apply_nextn, build_model_config +from aisimulate_core.sdk.config_builders import apply_nextn, build_model_config, validate_moe_controls from aisimulate_core.sdk.deepseek_v41 import MODEL_PATH as DEEPSEEK_V41_MODEL_PATH from aisimulate_core.sdk.errors import InvalidEngineConfigurationError as InvalidEngineConfigurationError from aisimulate_core.sdk.models import get_model @@ -465,6 +465,13 @@ def compile_engine( resolved_moe_tp = moe_tp_size if moe_tp_size is not None else 1 resolved_moe_ep = moe_ep_size if moe_ep_size is not None else 1 try: + validate_moe_controls( + model_path=model_path, + enable_eplb=enable_eplb, + wideep_num_slots=wideep_num_slots, + moe_backend=moe_backend, + moe_kernel_source=moe_kernel_source, + ) resolved_speculation = SpeculationConfig(**speculation) if speculation is not None else None model_config = build_model_config( tp_size=tp_size, @@ -490,11 +497,6 @@ def compile_engine( apply_nextn(model_config, nextn) except (ValueError, TypeError, KeyError) as exc: raise InvalidEngineConfigurationError(str(exc)) from exc - if enable_eplb or wideep_num_slots is not None or moe_backend not in (None, "default"): - from aisimulate_core.sdk.models import check_is_moe - - if not check_is_moe(model_path): - raise InvalidEngineConfigurationError("EPLB, slots and moe_backend require an MoE model") model_config.decoder_replay = decoder_replay try: resolve_dsv4_moe_arch(model_config, model_path, system_name=system, backend_name=backend) diff --git a/python/aisimulate/src/aisimulate_core/sdk/memory.py b/python/aisimulate/src/aisimulate_core/sdk/memory.py index 01e6ee9064..d2f8ead2b7 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/memory.py +++ b/python/aisimulate/src/aisimulate_core/sdk/memory.py @@ -310,7 +310,11 @@ def from_request( resolved_moe_tp = moe_tp_size if moe_tp_size is not None else 1 resolved_moe_ep = moe_ep_size if moe_ep_size is not None else 1 validate_moe_controls( - model_path=model_path, enable_eplb=enable_eplb, wideep_num_slots=wideep_num_slots, moe_backend=moe_backend + model_path=model_path, + enable_eplb=enable_eplb, + wideep_num_slots=wideep_num_slots, + moe_backend=moe_backend, + moe_kernel_source=moe_kernel_source, ) model_config = build_model_config( tp_size=tp_size, @@ -1092,7 +1096,11 @@ def estimate_kv_cache( # Validate the compute-side MTP depth before any fallback path. validate_nextn(nextn) validate_moe_controls( - model_path=model_path, enable_eplb=enable_eplb, wideep_num_slots=wideep_num_slots, moe_backend=moe_backend + model_path=model_path, + enable_eplb=enable_eplb, + wideep_num_slots=wideep_num_slots, + moe_backend=moe_backend, + moe_kernel_source=moe_kernel_source, ) try: diff --git a/python/aisimulate/src/aisimulate_core/sdk/models/__init__.py b/python/aisimulate/src/aisimulate_core/sdk/models/__init__.py index b5bff6580b..5bffc3c224 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/models/__init__.py +++ b/python/aisimulate/src/aisimulate_core/sdk/models/__init__.py @@ -28,6 +28,7 @@ import copy import importlib +import json import pkgutil from aisimulate_core.sdk import config @@ -64,6 +65,18 @@ _FORWARD_MODELS = ("op_level", "fpm") +def _uses_moe_kernel_source(spec, source: str) -> bool: + """Inspect native composite children as well as top-level MoE operators.""" + if isinstance(spec, dict): + moe = spec.get("Moe") + if isinstance(moe, dict) and moe.get("moe_kernel_source") == source: + return True + return any(_uses_moe_kernel_source(child, source) for child in spec.values()) + if isinstance(spec, list): + return any(_uses_moe_kernel_source(child, source) for child in spec) + return False + + def _apply_forward_model_fpm(model: BaseModel) -> BaseModel: """Centralized fpm rewrite: each phase list becomes exactly one whole-model op. No model class rewrites its own lists; metadata, parallelism, and the @@ -145,6 +158,16 @@ def get_model( raise InvalidEngineConfigurationError( f"Unknown forward_model: {forward_model!r}. Valid values: {', '.join(_FORWARD_MODELS)}" ) + if model_config.moe_kernel_source is not None: + from aisimulate_core.sdk.config_builders import validate_moe_controls + + if forward_model == "fpm": + raise InvalidEngineConfigurationError("moe_kernel_source is not supported with forward_model='fpm'") + validate_moe_controls( + model_path=model_path, + moe_backend=model_config.moe_backend, + moe_kernel_source=model_config.moe_kernel_source, + ) # Shallow-copy so mutations below don't poison the @cache'd original. model_info = dict(_get_model_info(model_path)) @@ -219,6 +242,14 @@ def get_model( model.spec_scheme = build_spec_scheme(model_config, spec_config) model.spec_scheme.validate(model, backend_name) materialize_spec_scheme(model) + if model_config.moe_kernel_source is not None: + for phase, phase_ops in (("context", model.context_ops), ("generation", model.generation_ops)): + if not any( + _uses_moe_kernel_source(json.loads(op._spec_json()), model_config.moe_kernel_source) for op in phase_ops + ): + raise InvalidEngineConfigurationError( + f"moe_kernel_source is not supported: {phase} graph has no compatible MoE operator" + ) if forward_model == "fpm": model = _apply_forward_model_fpm(model) return model diff --git a/python/aisimulate/src/aisimulate_core/sdk/models/blocks/moe.py b/python/aisimulate/src/aisimulate_core/sdk/models/blocks/moe.py index 4538baae67..0efc8735c7 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/models/blocks/moe.py +++ b/python/aisimulate/src/aisimulate_core/sdk/models/blocks/moe.py @@ -28,6 +28,7 @@ import aisimulate_core.sdk.operations as ops from aisimulate_core.sdk import common +from aisimulate_core.sdk.errors import InvalidEngineConfigurationError from aisimulate_core.sdk.models.helpers import check_is_moe from aisimulate_core.sdk.operations.moe_comm import MOE_A2A_BACKENDS, communication_dtype_for, nodes_for @@ -297,7 +298,9 @@ def _default_moe_block_ops( comm_backend = (getattr(cfg, "moe_comm_backend", None) or {}).get(inference_phase) if comm_backend: if getattr(cfg, "moe_kernel_source", None) is not None: - raise ValueError("moe_kernel_source is unsupported when moe_comm_backend selects the large-EP MoE graph") + raise InvalidEngineConfigurationError( + "moe_kernel_source is unsupported when moe_comm_backend selects the large-EP MoE graph" + ) return _large_ep_block_ops( comm_backend, prefix=prefix, diff --git a/python/aisimulate/tests/unit/sdk/models/test_model_config.py b/python/aisimulate/tests/unit/sdk/models/test_model_config.py index 26f0ab36f9..b1acaaec20 100644 --- a/python/aisimulate/tests/unit/sdk/models/test_model_config.py +++ b/python/aisimulate/tests/unit/sdk/models/test_model_config.py @@ -17,6 +17,7 @@ import aisimulate.sdk.operations as ops from aisimulate.sdk import common, config, models from aisimulate.sdk.config_builders import build_model_config +from aisimulate.sdk.errors import InvalidEngineConfigurationError from aisimulate.sdk.models import ( LLAMAModel, Qwen3VLModel, @@ -103,6 +104,39 @@ def test_model_config_rejects_invalid_kernel_source(source): config.ModelConfig(moe_kernel_source=source) +@pytest.mark.parametrize( + ("model_path", "moe_backend", "message"), + [ + ("Qwen/Qwen3-32B", None, "require an MoE model"), + ("deepseek-ai/DeepSeek-V4-Pro", "megamoe", "MegaMoE"), + ], +) +def test_model_graph_rejects_moe_kernel_source_without_a_compatible_operator(model_path, moe_backend, message): + model_config = config.ModelConfig( + tp_size=1, + attention_dp_size=8, + moe_tp_size=1, + moe_ep_size=8, + moe_backend=moe_backend, + moe_kernel_source="missing_source", + ) + + with pytest.raises(InvalidEngineConfigurationError, match=message): + get_model(model_path, model_config, "sglang") + + +def test_model_graph_rejects_source_when_layer_override_removes_all_moe_operators(): + # Kimi-K3 has a dense first layer; its checkpoint metadata still declares MoE. + model_path = "moonshotai/Kimi-K3" + assert check_is_moe(model_path) + model_config = config.ModelConfig( + overwrite_num_layers=1, moe_tp_size=1, moe_ep_size=1, moe_kernel_source="missing_source" + ) + + with pytest.raises(InvalidEngineConfigurationError, match="moe_kernel_source.*no compatible MoE"): + get_model(model_path, model_config, "sglang") + + class TestSupportedModels: """Test default models configuration from support_matrix.csv.""" diff --git a/python/aisimulate/tests/unit/sdk/models/test_moe_block_builder_followups.py b/python/aisimulate/tests/unit/sdk/models/test_moe_block_builder_followups.py index d0aefc1bc8..1ffbb4b214 100644 --- a/python/aisimulate/tests/unit/sdk/models/test_moe_block_builder_followups.py +++ b/python/aisimulate/tests/unit/sdk/models/test_moe_block_builder_followups.py @@ -23,6 +23,7 @@ import aisimulate_core.sdk.operations as ops from aisimulate_core.sdk import common, config +from aisimulate_core.sdk.errors import InvalidEngineConfigurationError from aisimulate_core.sdk.models import get_model from aisimulate_core.sdk.models.blocks.moe import MoEBlockShape, build_moe_block_ops @@ -61,7 +62,7 @@ def _dispatches(op_list): class TestGpusPerNodeGuard: def test_large_ep_rejects_an_explicit_moe_kernel_source(self): cfg = _cfg(moe_comm_backend={"context": "deepep_ht"}, moe_kernel_source="pinned_source") - with pytest.raises(ValueError, match="moe_kernel_source.*large-EP"): + with pytest.raises(InvalidEngineConfigurationError, match="moe_kernel_source.*large-EP"): _build(cfg, gpus_per_node=8) def test_large_ep_without_gpus_per_node_raises(self): diff --git a/python/aisimulate/tests/unit/sdk/test_compile_engine_mtp.py b/python/aisimulate/tests/unit/sdk/test_compile_engine_mtp.py index 7817421de6..7bedb3b3eb 100644 --- a/python/aisimulate/tests/unit/sdk/test_compile_engine_mtp.py +++ b/python/aisimulate/tests/unit/sdk/test_compile_engine_mtp.py @@ -124,7 +124,7 @@ def _capture_spec(model, **_kwargs): monkeypatch.setattr(engine.aisimulate_core, "engine_spec_bincode_from_json", lambda s: b"") engine.compile_engine( - "Qwen/Qwen3-32B", + "Qwen/Qwen3-30B-A3B", "h200_sxm", "trtllm", moe_kernel_source="sglang_flashinfer_trtllm_moe", diff --git a/python/aisimulate/tests/unit/sdk/test_moe_kernel_source_consumers.py b/python/aisimulate/tests/unit/sdk/test_moe_kernel_source_consumers.py index 0f77315a3c..39abd772b9 100644 --- a/python/aisimulate/tests/unit/sdk/test_moe_kernel_source_consumers.py +++ b/python/aisimulate/tests/unit/sdk/test_moe_kernel_source_consumers.py @@ -14,6 +14,7 @@ from aisimulate.sweeper.config import SearchSpace from aisimulate.sweeper.forward_pass_estimator import ForwardPassEstimatorResolver from aisimulate_core.sdk import RustForwardPassPerfModel +from aisimulate_core.sdk.errors import InvalidEngineConfigurationError pytestmark = pytest.mark.unit SOURCE = "sglang_flashinfer_trtllm_moe" @@ -135,7 +136,7 @@ def unavailable(*args, **kwargs): monkeypatch.setattr(memory.KVCacheEstimator, "from_request", unavailable) with pytest.raises(ValueError, match="unavailable model"): memory.estimate_num_gpu_blocks( - "model", + "Qwen/Qwen3-30B-A3B", "b200_sxm", "sglang", scheduler_block_size=1, @@ -146,3 +147,73 @@ def unavailable(*args, **kwargs): moe_kernel_source=SOURCE, allow_naive_fallback=True, ) + + +@pytest.mark.parametrize("entry_point", ["compile", "native_memory", "memory_with_fallback"]) +@pytest.mark.parametrize( + ("model_path", "moe_backend", "message"), + [ + ("Qwen/Qwen3-32B", None, "require an MoE model"), + ("deepseek-ai/DeepSeek-V4-Pro", "megamoe", "MegaMoE"), + ], +) +def test_unsupported_source_is_rejected_before_database_or_capacity_fallback( + entry_point, model_path, moe_backend, message +): + from aisimulate_core.sdk import engine, memory + + controls = {"moe_backend": moe_backend, "moe_kernel_source": SOURCE} + with pytest.raises(InvalidEngineConfigurationError, match=message): + if entry_point == "compile": + engine.compile_engine(model_path, "missing-system", "sglang", **controls) + elif entry_point == "native_memory": + memory.KVCacheEstimator.from_request( + model_path, "missing-system", "sglang", max_num_tokens=1, max_batch_size=1, **controls + ) + else: + memory.estimate_num_gpu_blocks( + model_path, + "missing-system", + "sglang", + scheduler_block_size=1, + max_num_tokens=1, + max_batch_size=1, + memory_fraction_kind="of_total", + memory_fraction_value=0.9, + allow_naive_fallback=True, + **controls, + ) + + +@pytest.mark.parametrize("estimation_mode", ["auto", "op_level"]) +@pytest.mark.parametrize( + ("model_path", "moe_backend", "message"), + [ + ("Qwen/Qwen3-32B", None, "require an MoE model"), + ("deepseek-ai/DeepSeek-V4-Pro", "megamoe", "MegaMoE"), + ], +) +def test_canonical_source_validation_cannot_fall_back(estimation_mode, model_path, moe_backend, message): + with pytest.raises(ValueError, match=f"invalid engine config:.*{message}"): + RustForwardPassPerfModel.best_available( + { + "model": model_path, + "system": "b200_sxm", + "backend": "sglang", + "worker_type": "aggregated", + "estimation_mode": estimation_mode, + "fallback_policy": "allow", + "moe_backend": moe_backend, + "moe_kernel_source": SOURCE, + } + ) + + +def test_task_cannot_rewrite_an_explicit_moe_source_into_whole_forward_fpm(): + from aisimulate.sdk.task_v2 import Task + from aisimulate_core.sdk.models import get_model + + task = Task(model_path="Qwen/Qwen3-30B-A3B", forward_model="fpm", moe_kernel_source=SOURCE) + model_config = task.build_model_config(role="agg") + with pytest.raises(InvalidEngineConfigurationError, match="moe_kernel_source.*forward_model='fpm'"): + get_model(task.model_path, model_config, "sglang") diff --git a/tests/sweeper/test_engine_request.py b/tests/sweeper/test_engine_request.py index 79f38c29ff..689d86353f 100644 --- a/tests/sweeper/test_engine_request.py +++ b/tests/sweeper/test_engine_request.py @@ -22,6 +22,7 @@ "enable_eplb": True, "wideep_num_slots": 256, "moe_backend": "deepep_moe", + "moe_kernel_source": "sglang_flashinfer_trtllm_moe", "attention_backend": "fa3", "gemm_quant_mode": "fp8", "moe_quant_mode": "fp8", diff --git a/tests/test_afd_runner.py b/tests/test_afd_runner.py index 7d39ee2ec7..742293404f 100644 --- a/tests/test_afd_runner.py +++ b/tests/test_afd_runner.py @@ -315,11 +315,21 @@ def test_single_output_token_completes_at_prefill_without_decode(phase, companio assert record["tpot_ms"] == 0.0 -def test_default_companion_model_consumes_fixed_timing_without_aic_lookup(): +@pytest.mark.parametrize( + "overrides", + [ + {}, + {"moe_kernel_source": None}, + {"aic_moe_kernel_source": None}, + {"attention_backend": "fa3", "enable_eplb": True}, + ], +) +def test_default_companion_model_consumes_fixed_timing_without_aic_lookup(overrides): spec = _spec( _topology(phase="prefill", combined_with_pd=True), companion_role="decode", ) + spec.backend_deployment.decode_engine_args.update(overrides) timing = AICAFDCompanionPerformanceModel().measure(spec) @@ -329,6 +339,58 @@ def test_default_companion_model_consumes_fixed_timing_without_aic_lookup(): assert timing.provenance["provider"] == "fixed" +@pytest.mark.parametrize(("phase", "companion_role"), [("decode", "prefill"), ("prefill", "decode")]) +@pytest.mark.parametrize("field", ["moe_kernel_source", "aic_moe_kernel_source"]) +@pytest.mark.parametrize("custom_timing", [False, True]) +def test_aic_companion_legacy_estimator_rejects_an_explicit_moe_source(phase, companion_role, field, custom_timing): + spec = _spec(_topology(phase=phase, combined_with_pd=True), companion_role=companion_role) + engine_args = getattr(spec.backend_deployment, f"{companion_role}_engine_args") + engine_args.pop("timing_model") + if custom_timing: + engine_args["timing_model"] = {"type": "polynomial"} + engine_args.update(aic_model_path="Qwen/Qwen3-30B-A3B", aic_system="b200_sxm") + engine_args[field] = "sglang_flashinfer_trtllm_moe" + + def estimator(*args, **kwargs): + pytest.fail("an explicit source must not reach an estimator that cannot consume it") + + with pytest.raises(InvalidRunnerError, match="moe_kernel_source.*not supported.*AFD companion"): + AICAFDCompanionPerformanceModel(estimator).measure(spec) + + +@pytest.mark.parametrize(("phase", "companion_role"), [("decode", "prefill"), ("prefill", "decode")]) +@pytest.mark.parametrize("field", ["moe_kernel_source", "aic_moe_kernel_source"]) +def test_aic_companion_external_fpm_preserves_source_for_canonical_rejection(phase, companion_role, field, tmp_path): + spec = _spec(_topology(phase=phase, combined_with_pd=True), companion_role=companion_role) + spec = replace( + spec, backend_deployment=replace(spec.backend_deployment, backend="sglang", backend_version="0.5.17") + ) + spec.backend_deployment.parallel_config[f"{companion_role}_tp"] = 1 + engine_args = getattr(spec.backend_deployment, f"{companion_role}_engine_args") + engine_args.pop("timing_model") + engine_args.update( + aic_model_path="Qwen/Qwen3-30B-A3B", + aic_system="b200_sxm", + aic_forward_model="fpm", + aic_fpm_parquet_path=str(tmp_path / "not-read.parquet"), + ) + engine_args[field] = "sglang_flashinfer_trtllm_moe" + + with pytest.raises(InvalidRunnerError, match="fpm_interpolation.*moe_kernel_source overrides"): + AICAFDCompanionPerformanceModel().measure(spec) + + +@pytest.mark.parametrize(("phase", "companion_role"), [("decode", "prefill"), ("prefill", "decode")]) +@pytest.mark.parametrize("field", ["moe_kernel_source", "aic_moe_kernel_source"]) +def test_aic_companion_fixed_timing_rejects_an_explicit_moe_source(phase, companion_role, field): + spec = _spec(_topology(phase=phase, combined_with_pd=True), companion_role=companion_role) + engine_args = getattr(spec.backend_deployment, f"{companion_role}_engine_args") + engine_args[field] = "sglang_flashinfer_trtllm_moe" + + with pytest.raises(ValueError, match="moe_kernel_source.*not supported.*fixed AFD companion timing"): + AICAFDCompanionPerformanceModel().measure(spec) + + @pytest.mark.parametrize(("phase", "companion_role"), [("decode", "prefill"), ("prefill", "decode")]) @pytest.mark.parametrize("forward_model", ["fpm", "op_level", None]) @pytest.mark.parametrize("field", ["forward_model", "aic_forward_model"]) From 26dd0c2b80bf0318cfba964147d652ec32101c37 Mon Sep 17 00:00:00 2001 From: Yiming Liu Date: Sat, 19 Sep 2026 00:05:20 +0800 Subject: [PATCH 06/14] collector: pin Qwen3.8 FP8 MoE runner Signed-off-by: Yiming Liu --- .../models/Qwen3_5MoeForCausalLM_cases.yaml | 13 +++++---- .../unit/collector/test_case_generator.py | 27 +++++++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 python/aisimulate/tests/unit/collector/test_case_generator.py diff --git a/python/aisimulate/collector/cases/models/Qwen3_5MoeForCausalLM_cases.yaml b/python/aisimulate/collector/cases/models/Qwen3_5MoeForCausalLM_cases.yaml index b9de8e14d9..e3e2516fa4 100644 --- a/python/aisimulate/collector/cases/models/Qwen3_5MoeForCausalLM_cases.yaml +++ b/python/aisimulate/collector/cases/models/Qwen3_5MoeForCausalLM_cases.yaml @@ -166,7 +166,7 @@ model_case_values: # attention override above, so it applies verbatim regardless of that # gap. # - # No sglang_moe_backends override is declared: the SM100 + # Without the deployment pin, the SM100 # flashinfer_trtllm moe-runner-backend forcer # (`_qwen3_moe_family_overrides`, arg_groups/overrides.py:1283-1290, # decorating :1291) is gated by the SAME kind of architecture-string @@ -184,10 +184,13 @@ model_case_values: # and Fp8MoEMethod.create_moe_runner (layers/quantization/fp8.py:2265-2294, # `is_deepgemm_moe_runner_backend_enabled()` requires a non-none a2a # backend so falls to `else: moe_runner_backend = MoeRunnerBackend.TRITON` - # at :2281) for fp8_block. That is exactly the - # base_ops/moe.yaml:79-81 default ({default: triton} for both - # bfloat16 and fp8_block already), so redeclaring it here would just - # duplicate the base value -- omitted instead. + # at :2281) for fp8_block. Keep that base Triton default for bfloat16. + # The FP8 deployment pins the runner instead: ai-dynamo/dynamo + # 5fc15ee, recipes/qwen3.8-2.4t-a95b/sglang/agg-gb200-chat/deploy.yaml: + # 101-112 (SM100) and agg-gb300-chat/deploy.yaml:12-30 (SM103) set + # fp8 quantization and moe-runner-backend: flashinfer_trtllm. + sglang_moe_backends: + fp8_block: {100: flashinfer_trtllm, 103: flashinfer_trtllm} # # vLLM has no schema field analogous to sglang_moe_backends: # collect_moe.py (collector/vllm/collect_moe.py) constructs FusedMoE diff --git a/python/aisimulate/tests/unit/collector/test_case_generator.py b/python/aisimulate/tests/unit/collector/test_case_generator.py new file mode 100644 index 0000000000..a5463454f0 --- /dev/null +++ b/python/aisimulate/tests/unit/collector/test_case_generator.py @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Regression coverage for model-specific collector case declarations.""" + +from __future__ import annotations + +import pytest + +from collector.case_generator import get_common_moe_test_cases, get_sglang_moe_backend + +pytestmark = pytest.mark.unit + + +QWEN38_MAX_FP8 = "Qwen/Qwen3.8-2.4T-A95B-FP8" + + +def test_qwen38_fp8_recipe_pins_only_blackwell_moe_runner(monkeypatch): + """The FP8 artifact follows its serving recipe; bf16 keeps the base default.""" + monkeypatch.setenv("COLLECTOR_MODEL_PATH", QWEN38_MAX_FP8) + + cases = get_common_moe_test_cases(backend="sglang") + assert cases + + for sm_version in (100, 103): + assert {get_sglang_moe_backend(case, "fp8_block", sm_version) for case in cases} == {"flashinfer_trtllm"} + assert {get_sglang_moe_backend(case, "bfloat16", sm_version) for case in cases} == {"triton"} From 9c4ca9de6b8a05204feec5ccfd6cf59c01de7363 Mon Sep 17 00:00:00 2001 From: Yiming Liu Date: Sun, 20 Sep 2026 20:41:59 +0800 Subject: [PATCH 07/14] data: add GB200 SGLang FlashInfer MoE measurements Signed-off-by: Yiming Liu --- .../moe/sglang/0.5.17/collection_meta.yaml | 24 +++++++++++++----- .../gb200/moe/sglang/0.5.17/moe_perf.parquet | Bin 78702 -> 101813 bytes 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/python/aisimulate/src/aisimulate_core/systems/data/gb200/moe/sglang/0.5.17/collection_meta.yaml b/python/aisimulate/src/aisimulate_core/systems/data/gb200/moe/sglang/0.5.17/collection_meta.yaml index 2a6f24d9e4..5f368e1ac6 100644 --- a/python/aisimulate/src/aisimulate_core/systems/data/gb200/moe/sglang/0.5.17/collection_meta.yaml +++ b/python/aisimulate/src/aisimulate_core/systems/data/gb200/moe/sglang/0.5.17/collection_meta.yaml @@ -33,13 +33,17 @@ tables: # - pipeline 63299076 (RadixArk id, nvfp4, kernel_source # sglang_flashinfer_trtllm_moe): 3159 rows landed, full grid, zero # failures. - # Concatenated 9153 rows total; keys disjoint by construction (moe_dtype - # partitions bfloat16/fp8_block/nvfp4, verified zero duplicate keys - # across all non-latency columns before landing) and disjoint from - # every other version. The archived result/collection_meta.yaml from - # each pipeline attests its distinct attempted case set; schema v2 keeps - # both events instead of collapsing their provenance. - rows: 9153 + # - Slurm job 7280873 (AIC-1781, collector branch 6c138f3, fp8_block, + # kernel_source sglang_flashinfer_trtllm_moe): 3159 rows, full grid, + # zero failures. This is the deployment-pinned FlashInfer TRT-LLM MoE + # lane and intentionally coexists with the stock fp8_block triton lane. + # Concatenated 12312 rows total; keys are disjoint by kernel_source and + # moe_dtype (verified zero duplicate keys across all non-latency columns) + # and disjoint from every other version. The archived + # result/collection_meta.yaml from each pipeline attests its distinct + # attempted case set; schema v2 keeps all three events instead of + # collapsing their provenance. + rows: 12312 status: complete collections: - collector_ref: cdbfd10329a1cf73bf07677ab25614dccfce2379 @@ -54,3 +58,9 @@ tables: collected_at: '2026-08-18' rows: 3159 status: complete + - collector_ref: 6c138f3bd73cd51f128c375c3055801ea4f24ef8 + collector_hash: sha256:9d62acbd82e19b5920557465ed815f9100e46fececca0970d1258fe6f1bfd744 + case_plan_hash: sha256:f2b134ac3e8fa59f0ff258abeeb0a9d3ff10c78f3adec0f1c8c0f95b1d62160d + collected_at: '2026-09-20' + rows: 3159 + status: complete diff --git a/python/aisimulate/src/aisimulate_core/systems/data/gb200/moe/sglang/0.5.17/moe_perf.parquet b/python/aisimulate/src/aisimulate_core/systems/data/gb200/moe/sglang/0.5.17/moe_perf.parquet index 982b3a461ac46b5f9e4369d23aab1197f0dfca4f..64618798886f530a5f6f2078dd609f58018f2ce0 100644 GIT binary patch delta 82214 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 delta 58911 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 Date: Thu, 24 Sep 2026 16:06:19 +0800 Subject: [PATCH 08/14] fix: validate optional MoE source controls Signed-off-by: Yiming Liu --- .../src/aisimulate/sweeper/config.py | 7 ++ .../aisimulate_core/sdk/models/blocks/moe.py | 2 +- .../test_moe_block_builder_followups.py | 14 ++++ .../sdk/test_moe_kernel_source_consumers.py | 72 ++++++++++++++++++- 4 files changed, 93 insertions(+), 2 deletions(-) diff --git a/python/aisimulate/src/aisimulate/sweeper/config.py b/python/aisimulate/src/aisimulate/sweeper/config.py index 2c161c2fbf..ce3e822271 100644 --- a/python/aisimulate/src/aisimulate/sweeper/config.py +++ b/python/aisimulate/src/aisimulate/sweeper/config.py @@ -824,6 +824,13 @@ def _validate_afd_contract(self) -> SearchSpace: def _normalize_estimator_database_mode(cls, value): return value.upper() if isinstance(value, str) else value + @field_validator("moe_kernel_source", mode="before") + @classmethod + def _validate_moe_kernel_source(cls, value): + from aisimulate_core.sdk.config import normalize_kernel_source + + return normalize_kernel_source(value, "moe_kernel_source") + @field_validator("systems_paths") @classmethod def _validate_estimator_roots(cls, value): diff --git a/python/aisimulate/src/aisimulate_core/sdk/models/blocks/moe.py b/python/aisimulate/src/aisimulate_core/sdk/models/blocks/moe.py index 0efc8735c7..d1b310c9fe 100644 --- a/python/aisimulate/src/aisimulate_core/sdk/models/blocks/moe.py +++ b/python/aisimulate/src/aisimulate_core/sdk/models/blocks/moe.py @@ -402,7 +402,7 @@ def _default_moe_block_ops( quant_mode, workload_distribution, cfg.attention_dp_size, - moe_kernel_source=cfg.moe_kernel_source, + moe_kernel_source=getattr(cfg, "moe_kernel_source", None), ), ops.MoEDispatch( f"{prefix}_moe_post_dispatch", diff --git a/python/aisimulate/tests/unit/sdk/models/test_moe_block_builder_followups.py b/python/aisimulate/tests/unit/sdk/models/test_moe_block_builder_followups.py index 1ffbb4b214..3b2d39bd65 100644 --- a/python/aisimulate/tests/unit/sdk/models/test_moe_block_builder_followups.py +++ b/python/aisimulate/tests/unit/sdk/models/test_moe_block_builder_followups.py @@ -19,6 +19,8 @@ from __future__ import annotations +from types import SimpleNamespace + import pytest import aisimulate_core.sdk.operations as ops @@ -59,6 +61,18 @@ def _dispatches(op_list): return [op for op in op_list if isinstance(op, ops.MoEDispatch)] +def test_fused_builder_accepts_config_without_optional_kernel_source(): + cfg = SimpleNamespace( + moe_tp_size=1, + moe_ep_size=8, + attention_dp_size=8, + moe_quant_mode=common.MoEQuantMode.bfloat16, + ) + built = _build(cfg) + expected = _build(_cfg()) + assert [op._spec_json() for op in built] == [op._spec_json() for op in expected] + + class TestGpusPerNodeGuard: def test_large_ep_rejects_an_explicit_moe_kernel_source(self): cfg = _cfg(moe_comm_backend={"context": "deepep_ht"}, moe_kernel_source="pinned_source") diff --git a/python/aisimulate/tests/unit/sdk/test_moe_kernel_source_consumers.py b/python/aisimulate/tests/unit/sdk/test_moe_kernel_source_consumers.py index 0806aea9ad..3276608f59 100644 --- a/python/aisimulate/tests/unit/sdk/test_moe_kernel_source_consumers.py +++ b/python/aisimulate/tests/unit/sdk/test_moe_kernel_source_consumers.py @@ -12,7 +12,10 @@ from aisimulate.config.engine import EnginePredictionConfig from aisimulate.runner import _materialize_engine_role from aisimulate.sweeper.config import SearchSpace -from aisimulate.sweeper.forward_pass_estimator import ForwardPassEstimatorResolver +from aisimulate.sweeper.forward_pass_estimator import ( + ForwardPassEstimatorResolutionError, + ForwardPassEstimatorResolver, +) from aisimulate_core.sdk import RustForwardPassPerfModel from aisimulate_core.sdk.errors import InvalidEngineConfigurationError @@ -20,6 +23,73 @@ SOURCE = "sglang_flashinfer_trtllm_moe" +@pytest.mark.parametrize("source", ["", " ", "\t\n"]) +def test_sweeper_rejects_blank_kernel_source_at_input(source): + with pytest.raises(ValueError, match="moe_kernel_source must be a non-empty string or None"): + SearchSpace(model_name="Qwen/Qwen3-30B-A3B", hardware_sku="b200_sxm", moe_kernel_source=source) + + +@pytest.mark.parametrize("source", [None, SOURCE, f" {SOURCE} "]) +def test_sweeper_round_trip_preserves_exact_kernel_source(source): + space = SearchSpace(model_name="Qwen/Qwen3-30B-A3B", hardware_sku="b200_sxm", moe_kernel_source=source) + restored = SearchSpace.model_validate_json(space.model_dump_json()) + assert restored.moe_kernel_source == source + + +@pytest.mark.parametrize( + ("mode", "fallback_policy"), + [ + ("fpm_regression", "deny"), + ("fpm_regression", "allow"), + ("auto", "deny"), + ("auto", "allow"), + ("op_level", "allow"), + ], +) +def test_untrained_regression_preserves_source_but_cannot_run_offline(tmp_path, mode, fallback_policy): + controls = { + "backend_version": "0.5.17", + "estimation_mode": mode, + "fallback_policy": fallback_policy, + "moe_kernel_source": SOURCE, + # The empty root makes op-level data unavailable without a network dependency. + "systems_paths": [str(tmp_path)], + } + model = RustForwardPassPerfModel.best_available( + {"model": "Qwen/Qwen3-30B-A3B", "system": "gb300", "backend": "sglang", "worker_type": "aggregated", **controls} + ) + try: + diagnostics = model.diagnostics() + assert diagnostics["provenance"]["selected_estimation_mode"] == "fpm_regression" + assert diagnostics["provenance"]["config"]["moe_kernel_source"] == SOURCE + assert diagnostics["readiness"] != "ready" + assert ( + model.estimate_forward_pass_time_ms( + {"version": 1, "scheduled_requests": {"num_decode_requests": 1, "sum_decode_kv_tokens": 128}} + ) + is None + ) + finally: + model.close() + + space = SearchSpace(model_name="Qwen/Qwen3-30B-A3B", hardware_sku="gb300", backend=["sglang"], **controls) + sample = { + "backend": "sglang", + "deployment_mode": "agg", + "hardware_sku": "gb300", + "tp": 1, + "pp": 1, + "attention_dp": 1, + "moe_tp": 1, + "moe_ep": 1, + "agg_block_size": 1, + } + with pytest.raises( + ForwardPassEstimatorResolutionError, match="not ready; regression requires training observations" + ): + ForwardPassEstimatorResolver(space).resolve_candidate(sample) + + @pytest.mark.parametrize("source", [None, SOURCE]) def test_cli_saved_config_and_replay_preserve_source(source): engine = EnginePredictionConfig( From ae53fcc75593b9f3ef1ef1b8c178b5184c614a16 Mon Sep 17 00:00:00 2001 From: Yiming Liu Date: Thu, 24 Sep 2026 16:37:34 +0800 Subject: [PATCH 09/14] fix: mark gb200 FP8 FlashInfer measurements opt-in Signed-off-by: Yiming Liu --- .../moe/sglang/0.5.17/collection_meta.yaml | 5 +++++ .../gb200/moe/sglang/0.5.17/moe_perf.parquet | Bin 101813 -> 102067 bytes 2 files changed, 5 insertions(+) diff --git a/python/aisimulate/src/aisimulate_core/systems/data/gb200/moe/sglang/0.5.17/collection_meta.yaml b/python/aisimulate/src/aisimulate_core/systems/data/gb200/moe/sglang/0.5.17/collection_meta.yaml index 5f368e1ac6..9971d2b600 100644 --- a/python/aisimulate/src/aisimulate_core/systems/data/gb200/moe/sglang/0.5.17/collection_meta.yaml +++ b/python/aisimulate/src/aisimulate_core/systems/data/gb200/moe/sglang/0.5.17/collection_meta.yaml @@ -37,6 +37,11 @@ tables: # kernel_source sglang_flashinfer_trtllm_moe): 3159 rows, full grid, # zero failures. This is the deployment-pinned FlashInfer TRT-LLM MoE # lane and intentionally coexists with the stock fp8_block triton lane. + # The optional Boolean default_eligible column is false for all 3159 + # fp8_block FlashInfer TRT-LLM rows, including the 324 TP32 rows; these + # rows require explicit kernel_source selection. It is true for the + # other 9153 rows. This selection annotation preserves every recorded + # measurement and the collection provenance below. # Concatenated 12312 rows total; keys are disjoint by kernel_source and # moe_dtype (verified zero duplicate keys across all non-latency columns) # and disjoint from every other version. The archived diff --git a/python/aisimulate/src/aisimulate_core/systems/data/gb200/moe/sglang/0.5.17/moe_perf.parquet b/python/aisimulate/src/aisimulate_core/systems/data/gb200/moe/sglang/0.5.17/moe_perf.parquet index 64618798886f530a5f6f2078dd609f58018f2ce0..50805e7275412109790e010f6e054efd39ca0f4a 100644 GIT binary patch delta 1194 zcmZWpU2hvz5WP1lR-zgyL9DeFp*0GPLQS)djY-@V#H=^Algh1a5$}goQSbUIWEYmk zv8i875p7A7Jn#VfP*wB~AS%%+FFYUweW>^m2>u01MQ851YO9W=dp({rbI&<@*AH$- zez_C*>OP;lzQLp)Wa-0qrF*||iAfPDGAnZ~a~5L@VxO5|j4eD+9$z{B%1?|L$1p`K z|DUr7cJre@dq;myO2^_yGg&4_ww-RvYe}=X@ zi^Z@%J{v!pKD55(mm{NjHY>6CsR)a|43?sVj{PIY_W1lC-$c*9#4e7__?ckm--*QM zi5oJE{r>YbHO^1Q9^Qzu_^cd-TJ4g4He@LOHKkBwK?4Z>{g$5_sX(y}1ArA+11MMX z0C^KS02bvM5`6X)4e@<(Y77Ygh6#&*N8AYMAY9bnQTBvax z1j<0M=2~81$u8B}mj0!0rQJ=txFH}V8nEX38n#h7H4aQ8QXf49ORa{Kv@va8PomsV}uH zqdTyS^^`M!6nRew5c*V8>4y}d8~nJ>rMHB@W-&Kt2p83@7aQ_x!HqqBa^V7vXz1A# z&TTts_k9b`^(qJ!Z6VEmWjQ$7<#VI1Z=uJwJ7!xRBJMf16)idTHy`>dG>ETs3;;b_ zRY*Z*S1mm|#2?V@0pQIj0o{}^#_FHWUoD6si|Vr*5#qLRQ+sy0-nL4$tP?(LhotHi zCHQrhpGXjgV$XHFtZSpqcBX#KOp|G9nQ)uIqkaDNYiN{_&US5m1Dd3MSqrZ0^OG@? zfLoSf`J!o^7%67(J=R7@7;maqzlOf`?T)reH6)6|J9K@pF8Ed!&TD>!l?+2X&K0W!~Vrnske+0pI2mFoT?g5X| OGBw4tR~f57ZRrWJkWH8X delta 1013 zcmZWoUvJV-6mJtlTtebv;sOyF31*2~wnAZUXku~-qre7s&7{!8hqkaWR#=5m*j~nr zFG}X#?1S+;nD}QeKKN1m6h7_2b1uvL- zF5v50je5(t7rq>v@nL>u?(v%`4AUk-$)&q?7`yC%O+vTH*SWa4U`M8!VLPq|`^uX4bi{`jh=pwTYM7^zC^_{O)=_9;vZ?B~PQ23*|M2wC z(9{F$*QEGN@n-RvzTi%p-e5@&Xq{^MCSk@bp}K}x?peqPdtRjZ;0STr6B#$7JBcqz zDxJ5$qHQW*fv~Bi?24$y>J`Z*oFl67YYH1#d^0Qyu1qBe5W`RE=#@fG*9J%^6}MU? z=D$P!$)&@i=f!b}u8 Date: Thu, 24 Sep 2026 16:39:00 +0800 Subject: [PATCH 10/14] fix: add explicit MoE default eligibility metadata Keep opt-in measurements queryable by exact source without changing automatic selection. Preserve annotations through ingestion and exclude metadata from measurement identity. Contributes to AIC-1781. Signed-off-by: Yiming Liu --- crates/core/src/perfmodel/engine/readiness.rs | 84 +++++- .../core/src/perfmodel/perf_database/moe.rs | 72 ++++- .../src/perfmodel/perf_database/table_view.rs | 12 +- docs/core-api.md | 2 + python/aisimulate/collector/README.md | 2 + python/aisimulate/collector/helper.py | 47 ++- .../test_perf_output_finalization.py | 50 ++++ .../database/test_moe_default_eligibility.py | 274 ++++++++++++++++++ .../unit/tools/test_check_cross_backend.py | 14 + .../tests/unit/tools/test_parquet_diff.py | 10 + .../tools/perf_database/parquet_diff.py | 4 +- .../tools/perf_database/perf_data_layout.py | 2 +- 12 files changed, 552 insertions(+), 21 deletions(-) create mode 100644 python/aisimulate/tests/unit/sdk/database/test_moe_default_eligibility.py diff --git a/crates/core/src/perfmodel/engine/readiness.rs b/crates/core/src/perfmodel/engine/readiness.rs index cdec2cb04e..f53ecf3e2f 100644 --- a/crates/core/src/perfmodel/engine/readiness.rs +++ b/crates/core/src/perfmodel/engine/readiness.rs @@ -7,9 +7,10 @@ use std::collections::HashMap; use crate::AicError; -use crate::common::enums::{DatabaseMode, GemmQuantMode, TransferKind}; +use crate::common::enums::{DatabaseMode, GemmQuantMode, MoeQuantMode, TransferKind}; use crate::config::PerfSource; use crate::operators::Op; +use crate::perf_database::moe::MoeKernel; use crate::perf_database::{PerfDatabase, kernel_source_ok, parquet_loader::PerfReader}; pub(super) fn validate<'a>( @@ -36,6 +37,15 @@ impl Availability<'_> { if let Some(found) = self.tables.get(name) { return Ok(*found); } + if name == "moe_perf.parquet" { + let found = match self.db.moe.available_quants(MoeKernel::Standard) { + Ok(quants) => !quants.is_empty(), + Err(error) if error.is_missing_perf_data() => false, + Err(error) => return Err(error), + }; + self.tables.insert(name.to_owned(), found); + return Ok(found); + } let sources = match name { "nccl_perf.parquet" => self .db @@ -184,7 +194,30 @@ impl Availability<'_> { MlaModuleContext(_) => self.any(&["mla_context_module_perf.parquet"]), MlaModuleGeneration(_) => self.any(&["mla_generation_module_perf.parquet"]), MlaBmm(_) => self.any(&["mla_bmm_perf.parquet"]), - Moe(_) => self.any(&["moe_perf.parquet"]), + Moe(moe) => { + let found = match moe.moe_kernel_source.as_deref() { + Some(source) => !self + .db + .moe + .available_quants_for_kernel_source(source)? + .is_empty(), + None => { + self.has("moe_perf.parquet")? + || (moe.is_gated + && moe.quant_mode == MoeQuantMode::Nvfp4 + && self.db.moe.low_latency_available()?) + } + }; + if found { + Ok(()) + } else { + Err(AicError::PerfDatabase(format!( + "required MoE data unavailable for kernel_source={:?} at {}", + moe.moe_kernel_source, + self.db.data_root.display() + ))) + } + } // State-space kernels explicitly fall back to their analytic SOL // on missing tables in every database mode. Mamba2(_) | Gdn(_) | Kda(_) => Ok(()), @@ -571,4 +604,51 @@ mod tests { } } } + + #[test] + fn moe_readiness_requires_eligible_or_exact_source_rows() { + use crate::common::enums::MoeQuantMode; + use crate::operators::MoeOp; + + let root = systems(); + let data = root + .path() + .join("data/b200_sxm/vllm/0.24.0/moe_perf.parquet"); + for eligibility in [None, Some(true), Some(false)] { + let mut columns = vec![ + Col::Str("moe_dtype", vec!["fp8_block"]), + Col::I64("num_tokens", vec![32]), + Col::I64("hidden_size", vec![8192]), + Col::I64("inter_size", vec![2048]), + Col::I64("topk", vec![8]), + Col::I64("num_experts", vec![256]), + Col::I64("moe_tp_size", vec![1]), + Col::I64("moe_ep_size", vec![1]), + Col::Str("distribution", vec!["uniform"]), + Col::Str("kernel_source", vec!["exact"]), + Col::F64("latency", vec![0.25]), + ]; + if let Some(eligible) = eligibility { + columns.push(Col::Bool("default_eligible", vec![eligible])); + } + write_parquet(&data, &columns); + for source in [None, Some("exact")] { + let db = PerfDatabase::load(root.path(), "b200_sxm", "vllm", "0.24.0").unwrap(); + let mut op = MoeOp::new( + "moe", + 8192, + 2048, + 8, + 256, + 1, + 1, + MoeQuantMode::Fp8Block, + "uniform", + ); + op.moe_kernel_source = source.map(str::to_owned); + let expected = source.is_some() || eligibility != Some(false); + assert_eq!(engine_readiness(db, Op::Moe(op)).is_ok(), expected); + } + } + } } diff --git a/crates/core/src/perfmodel/perf_database/moe.rs b/crates/core/src/perfmodel/perf_database/moe.rs index 52e5013683..fa1e99c836 100644 --- a/crates/core/src/perfmodel/perf_database/moe.rs +++ b/crates/core/src/perfmodel/perf_database/moe.rs @@ -39,7 +39,7 @@ use super::{SourceResolver, kernel_source_ok}; use crate::common::enums::MoeQuantMode; use crate::common::error::AicError; use crate::config::{PerfDbSources, PerfSource}; -use crate::perf_database::parquet_loader::PerfReader; +use crate::perf_database::parquet_loader::{PerfReader, PerfRow}; pub struct MoeTable { data_root: PathBuf, @@ -75,8 +75,9 @@ pub struct MoeSiblingSlice { } /// Parallel grids split by `kernel_source`. `default` and `low_latency` -/// preserve the legacy automatic lookup behavior, while `by_kernel_source` -/// retains every named lane for an exact user-selected lookup. +/// contain default-eligible rows (all legacy rows when the metadata column is +/// absent), while `by_kernel_source` retains every named lane for an exact +/// user-selected lookup. struct LoadedMoeGrids { default: MoeGrids, low_latency: MoeGrids, @@ -611,6 +612,37 @@ pub(crate) fn moe_kernel_quant_rewrite(raw_quant: String, kernel_source: &str) - } } +/// Optional selection metadata shared by the query loader and coverage view. +/// An absent column preserves legacy automatic selection; malformed metadata +/// is not a coverage miss and must never trigger an estimator fallback. +pub(crate) fn moe_default_eligible( + reader: &PerfReader, + row: &PerfRow, + column: Option, + kernel_source_column: Option, +) -> Result { + let Some(column) = column else { + return Ok(true); + }; + let eligible = row.bool_strict(column).map_err(|_| { + AicError::InvalidPerfData(format!( + "default_eligible must be a non-null Boolean at {}", + reader.path().display() + )) + })?; + if !eligible + && row + .str_optional(kernel_source_column)? + .is_none_or(|source| source.trim().is_empty()) + { + return Err(AicError::InvalidPerfData(format!( + "default_eligible=false requires a nonblank string kernel_source at {}", + reader.path().display() + ))); + } + Ok(eligible) +} + fn load_moe_parquet(sources: &[PerfSource]) -> Result { let mut default_index: MoeIndex> = MoeIndex::default(); let mut low_latency_index: MoeIndex> = @@ -645,11 +677,14 @@ fn load_moe_parquet(sources: &[PerfSource]) -> Result // the `default` grid (matching the pre-split behavior). The same column // gates the per-source shared-layer `kernel_source` allowlist. let kernel_source_col = reader.col_optional("kernel_source"); + let default_eligible_col = reader.col_optional("default_eligible"); for row in reader.rows()? { let row = row?; if !kernel_source_ok(source.kernel_sources(), kernel_source_col, &row)? { continue; } + let default_eligible = + moe_default_eligible(&reader, &row, default_eligible_col, kernel_source_col)?; let kernel_source = row .str_optional(kernel_source_col)? .unwrap_or("") @@ -683,15 +718,17 @@ fn load_moe_parquet(sources: &[PerfSource]) -> Result let power = row.f64_optional(power_col)?.unwrap_or(0.0); let value = LeafValue::with_power(latency, power); let num_tokens = row.u32(num_tokens_col)?; - insert_moe_row( - target, - target_quants, - &quant, - &distribution, - shape, - num_tokens, - value, - ); + if default_eligible { + insert_moe_row( + target, + target_quants, + &quant, + &distribution, + shape, + num_tokens, + value, + ); + } if !kernel_source.is_empty() { let source_index = kernel_source_indices .entry(kernel_source.clone()) @@ -709,7 +746,11 @@ fn load_moe_parquet(sources: &[PerfSource]) -> Result } } } - if !any_source || (default_index.is_empty() && low_latency_index.is_empty()) { + if !any_source + || (default_index.is_empty() + && low_latency_index.is_empty() + && kernel_source_indices.is_empty()) + { return Err(AicError::PerfDatabase(format!( "no rows loaded from {} source(s) (first: {})", sources.len(), @@ -749,7 +790,10 @@ fn load_moe_parquet(sources: &[PerfSource]) -> Result } fn clone_err(err: &AicError) -> AicError { - AicError::PerfDatabase(err.to_string()) + match err { + AicError::InvalidPerfData(message) => AicError::InvalidPerfData(message.clone()), + _ => AicError::PerfDatabase(err.to_string()), + } } #[cfg(test)] diff --git a/crates/core/src/perfmodel/perf_database/table_view.rs b/crates/core/src/perfmodel/perf_database/table_view.rs index d2205735fd..ffaa5f831d 100644 --- a/crates/core/src/perfmodel/perf_database/table_view.rs +++ b/crates/core/src/perfmodel/perf_database/table_view.rs @@ -912,12 +912,22 @@ pub fn view_kda(sources: &[PerfSource]) -> Result, AicError> { /// `operations/moe.py::load_moe_data` — 9-level /// `[moe_dtype][distribution][topk][num_experts][hidden][inter][moe_tp][moe_ep][num_tokens]`, /// with `kernel_source == "moe_torch_flow_min_latency"` rows routed to the -/// low-latency twin table. Returns (default, low_latency). +/// low-latency twin table. Only default-eligible rows enter these automatic +/// coverage views; raw parquet enumeration retains every measured row. +/// Returns (default, low_latency). pub fn view_moe(sources: &[PerfSource]) -> Result, AicError> { let mut default = ViewNode::branch(); let mut low_latency = ViewNode::branch(); let found = fold_sources(sources, |ctx| { let r = ctx.reader; + if !crate::perf_database::moe::moe_default_eligible( + r, + ctx.row, + r.col_optional("default_eligible"), + ctx.ks_col, + )? { + return Ok(()); + } let num_tokens = ctx.row.u32(r.col("num_tokens")?)?; let hidden = ctx.row.u32(r.col("hidden_size")?)?; let inter = ctx.row.u32(r.col("inter_size")?)?; diff --git a/docs/core-api.md b/docs/core-api.md index 042c2a1149..2b4e5bcb21 100644 --- a/docs/core-api.md +++ b/docs/core-api.md @@ -212,6 +212,8 @@ The canonical configuration also carries quantization overrides and `attention_b `moe_kernel_source` selects an exact, nonblank collected `kernel_source` label for fused MoE compute. It is distinct from the existing `moe_backend` graph/backend control; source labels are not backend aliases and are preserved without trimming. `None` keeps the existing default source-selection policy, including eligible low-latency NVFP4 selection. `SILICON` reads only the requested source's table; `EMPIRICAL` derives its estimate from that same source; `HYBRID` may fall back to empirical estimation within that source, but does not substitute a different source. Missing source data remains an error. An explicit `moe_torch_flow_min_latency` requires gated NVFP4 and at most 128 tokens after attention-DP gathering. Pure-roofline `SOL` remains table-independent and does not claim measured support for the requested source. +`moe_perf.parquet` may include Boolean selection metadata `default_eligible`. An absent column preserves legacy automatic selection; when present, every value must be a non-null Boolean. A `false` row requires a nonblank string `kernel_source`, preserved exactly, and is available only through that named source. It cannot enter automatic standard or low-latency grids, including empirical cross-shape and cross-quant reference selection. Among eligible rows, existing source priority and first-row precedence remain unchanged. Default table views, coverage, and readiness use those same eligible grids; raw Parquet enumeration retains all measured rows. Malformed eligibility metadata is an invalid-data error, never a missing-data fallback. The flag is not a measurement identity dimension. Collector finalization preserves it during merges, including existing annotations when a legacy recollection omits the column; genuinely new legacy keys remain eligible. + An explicit source is rejected for dense graphs, MegaMoE modules, large-EP expert-compute graphs, and any constructed timing phase with no compatible fused MoE operator. It is also incompatible with whole-forward FPM, including the legacy Task `forward_model='fpm'` rewrite. Invalid graph/source combinations fail as invalid configuration rather than triggering estimator fallback. An untrained `fpm_regression` model remains not-ready; retaining a source in its configuration is not evidence of source-specific prediction support. AFD regular companions currently reject exact-source requests through their legacy estimator and fixed timing paths. The external-FPM companion forwards the source to canonical validation, which rejects the incompatible FPM request. These controls describe standalone AISimulate behavior, not downstream Dynamo planner integration. diff --git a/python/aisimulate/collector/README.md b/python/aisimulate/collector/README.md index 1341e89ef6..f0bc8f9dda 100644 --- a/python/aisimulate/collector/README.md +++ b/python/aisimulate/collector/README.md @@ -587,6 +587,8 @@ Large-EP MoE uses stock `moe_perf` for modeled local expert compute and `moe_a2a_perf` records latency in microseconds; `load_moe_a2a_data` converts leaves to milliseconds. Stock `moe_perf` retains its existing timing contract. +Stock `moe_perf` also supports optional Boolean `default_eligible` selection metadata. The [Core API contract](../../../docs/core-api.md#choosing-a-forward-pass-api) defines automatic and exact-source selection. Finalization validates non-null Boolean flags and the named source required by `false`, excludes the flag from measurement identity, and preserves existing annotations when merging a legacy collection that omits the column. Collectors do not infer eligibility from kernel labels or row order. + **Legacy-overwrite caveats.** A new-schema row replaces a legacy-adapted leaf only at the *same* key, and the legacy adapters derive their node/EP geometry rather than reading it: diff --git a/python/aisimulate/collector/helper.py b/python/aisimulate/collector/helper.py index 321ec3b558..24f64931f7 100644 --- a/python/aisimulate/collector/helper.py +++ b/python/aisimulate/collector/helper.py @@ -962,7 +962,7 @@ def convert_perf_csv_to_parquet( When ``merge_existing`` is True and a parquet already exists at the target, the new rows are merged into it instead of overwriting: the existing and new rows are concatenated (new last) and deduplicated on their identity key — - every column except the measured metrics (``PERF_METRIC_COLUMNS``) — keeping + every column except measured metrics and MoE selection metadata — keeping the newest row per key. This makes finalization idempotent and accumulative: a resumed / ``--resume-retry-failed`` / batched collection extends the parquet instead of clobbering it with only the current run's subset. @@ -1885,6 +1885,7 @@ def _prepare_perf_file( raise RuntimeError(f"Collector staging file changed after preflight: {csv_path}") snapshot_file.seek(0) table = pc_csv.read_csv(snapshot_file) + _validate_moe_default_eligibility(table, parquet_path, pa=pa) new_rows = table.num_rows merged_existing = False merge_target = None @@ -2523,6 +2524,23 @@ def cleanup_unjournaled_perf_preparations( return True +def _validate_moe_default_eligibility(table, parquet_path: Path, *, pa): + if parquet_path.name != "moe_perf.parquet" or "default_eligible" not in table.column_names: + return + flags = table.column("default_eligible") + if not pa.types.is_boolean(flags.type) or flags.null_count: + raise ValueError(f"{parquet_path}: default_eligible must contain only non-null Boolean values") + if "kernel_source" in table.column_names: + sources = table.column("kernel_source").to_pylist() + else: + sources = [None] * table.num_rows + if any( + not eligible and (not isinstance(source, str) or not source.strip()) + for eligible, source in zip(flags.to_pylist(), sources, strict=True) + ): + raise ValueError(f"{parquet_path}: default_eligible=false requires a nonblank string kernel_source") + + def _merge_perf_rows(new_table, old_table, parquet_path: Path, *, pa): """Merge freshly-collected rows into an existing perf parquet, keeping the newest row per identity key. Returns ``(table, merged_existing, @@ -2531,6 +2549,25 @@ def _merge_perf_rows(new_table, old_table, parquet_path: Path, *, pa): import pandas as pd log = logging.getLogger(__name__) + _validate_moe_default_eligibility(old_table, parquet_path, pa=pa) + + # Eligibility annotates a measurement; it does not create a new identity. + # Legacy rows are eligible, but a legacy recollection must not erase an + # existing annotation on the same physical key. + metadata_columns = {"default_eligible"} if parquet_path.name == "moe_perf.parquet" else set() + inherit_eligibility = False + if metadata_columns: + old_has_flag = "default_eligible" in old_table.column_names + new_has_flag = "default_eligible" in new_table.column_names + inherit_eligibility = old_has_flag and not new_has_flag + if old_has_flag and not new_has_flag: + new_table = new_table.append_column( + "default_eligible", pa.array([True] * new_table.num_rows, type=pa.bool_()) + ) + elif new_has_flag and not old_has_flag: + old_table = old_table.append_column( + "default_eligible", pa.array([True] * old_table.num_rows, type=pa.bool_()) + ) # Compare (name, type) pairs for IDENTITY columns only: matching names # with drifted types would otherwise round-trip through pandas and @@ -2586,7 +2623,13 @@ def fields(schema): old_df = old_table.to_pandas() new_df = new_df[old_df.columns.tolist()] # align column order - identity = [c for c in old_df.columns if c not in PERF_METRIC_COLUMNS] + identity = [c for c in old_df.columns if c not in PERF_METRIC_COLUMNS and c not in metadata_columns] + if inherit_eligibility: + annotations = old_df.drop_duplicates(subset=identity, keep="last")[identity + ["default_eligible"]] + new_df = new_df.drop(columns="default_eligible").merge(annotations, on=identity, how="left", sort=False) + flags = new_df["default_eligible"] + new_df["default_eligible"] = flags.where(flags.notna(), True).astype(bool) + new_df = new_df[old_df.columns.tolist()] current_event_rows = len(new_df.drop_duplicates(subset=identity, keep="last")) combined = pd.concat([old_df, new_df], ignore_index=True) deduped = combined.drop_duplicates(subset=identity, keep="last").reset_index(drop=True) diff --git a/python/aisimulate/tests/unit/collector/test_perf_output_finalization.py b/python/aisimulate/tests/unit/collector/test_perf_output_finalization.py index b2714fe6dd..3205f270ab 100644 --- a/python/aisimulate/tests/unit/collector/test_perf_output_finalization.py +++ b/python/aisimulate/tests/unit/collector/test_perf_output_finalization.py @@ -794,6 +794,56 @@ def test_finalize_merge_replaces_same_key_with_newest_measurement(tmp_path): assert sorted(shapes) == ["s1", "s2", "s3"] +def test_moe_eligibility_is_metadata_when_merging_measurements(tmp_path): + perf = tmp_path / "moe_perf.txt" + perf.write_text("kernel_source,shape,latency,default_eligible\nexact,s1,1.0,true\nexact,s2,2.0,false\n") + [parquet] = finalize_perf_files([perf]) + perf.write_text("kernel_source,shape,latency,default_eligible\nexact,s1,3.0,false\n") + finalize_perf_files([perf]) + + assert pq.read_table(parquet).to_pylist() == [ + {"kernel_source": "exact", "shape": "s2", "latency": 2.0, "default_eligible": False}, + {"kernel_source": "exact", "shape": "s1", "latency": 3.0, "default_eligible": False}, + ] + + +@pytest.mark.parametrize("existing_annotated", [False, True]) +def test_moe_eligibility_survives_merges_with_legacy_schemas(tmp_path, existing_annotated): + perf = tmp_path / "moe_perf.txt" + if existing_annotated: + perf.write_text("kernel_source,shape,latency,default_eligible\nexact,s1,1.0,false\nexact,s2,2.0,false\n") + else: + perf.write_text("kernel_source,shape,latency\nexact,s1,1.0\nexact,s2,2.0\n") + [parquet] = finalize_perf_files([perf]) + if existing_annotated: + perf.write_text("kernel_source,shape,latency\nexact,s1,3.0\nexact,s3,4.0\n") + else: + perf.write_text("kernel_source,shape,latency,default_eligible\nexact,s1,3.0,false\nexact,s3,4.0,true\n") + finalize_perf_files([perf]) + + rows = {row["shape"]: row for row in pq.read_table(parquet).to_pylist()} + assert set(rows) == {"s1", "s2", "s3"} + assert rows["s1"]["latency"] == 3.0 + assert rows["s1"]["default_eligible"] is False + assert rows["s2"]["latency"] == 2.0 + assert rows["s2"]["default_eligible"] is (not existing_annotated) + assert rows["s3"]["default_eligible"] is True + + +@pytest.mark.parametrize("row", ["exact,s1,1.0,0", "exact,s1,1.0,", "exact,s1,1.0,invalid", ",s1,1.0,false"]) +def test_moe_eligibility_validation_precedes_publication(tmp_path, row): + perf = tmp_path / "moe_perf.txt" + perf.write_text("kernel_source,shape,latency,default_eligible\nexact,s1,2.0,true\n") + [parquet] = finalize_perf_files([perf]) + previous = parquet.read_bytes() + perf.write_text(f"kernel_source,shape,latency,default_eligible\n{row}\n") + + with pytest.raises(ValueError, match="default_eligible"): + finalize_perf_files([perf]) + assert parquet.read_bytes() == previous + assert perf.is_file() + + def test_finalize_merge_attests_deduplicated_current_identity_count(tmp_path): perf = tmp_path / "gemm_perf.txt" parquet = perf.with_suffix(".parquet") diff --git a/python/aisimulate/tests/unit/sdk/database/test_moe_default_eligibility.py b/python/aisimulate/tests/unit/sdk/database/test_moe_default_eligibility.py new file mode 100644 index 0000000000..b72e18f354 --- /dev/null +++ b/python/aisimulate/tests/unit/sdk/database/test_moe_default_eligibility.py @@ -0,0 +1,274 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""MoE selection metadata through the public database and native query path.""" + +import json +import shutil +from pathlib import Path + +import pyarrow as pa +import pyarrow.parquet as pq +import pytest + +from aisimulate_core.sdk.common import MoEQuantMode +from aisimulate_core.sdk.engine import EngineHandle +from aisimulate_core.sdk.engine_table_view import fetch_table_view +from aisimulate_core.sdk.errors import EmpiricalNotImplementedError, PerfDataNotAvailableError +from aisimulate_core.sdk.perf_database import get_database_view + +pytestmark = pytest.mark.unit + +SYSTEMS = Path(__file__).resolve().parents[4] / "src/aisimulate_core/systems" +SOURCE = "sglang_flashinfer_trtllm_moe" + + +@pytest.fixture +def systems_root(tmp_path): + shutil.copyfile(SYSTEMS / "b200_sxm.yaml", tmp_path / "b200_sxm.yaml") + return tmp_path + + +def _row(**overrides): + return { + "moe_dtype": "fp8_block", + "num_tokens": 32, + "hidden_size": 8192, + "inter_size": 2048, + "topk": 8, + "num_experts": 256, + "moe_tp_size": 1, + "moe_ep_size": 1, + "distribution": "uniform", + "kernel_source": SOURCE, + "latency": 0.25, + **overrides, + } + + +def _write(root, rows, *, backend="sglang", version="0.5.17"): + path = root / f"data/b200_sxm/moe/{backend}/{version}/moe_perf.parquet" + path.parent.mkdir(parents=True, exist_ok=True) + pq.write_table(pa.Table.from_pylist(rows), path) + return path + + +def _database(root, *, mode="SILICON", shared_layer=False): + # Synthetic measurement rows intentionally omit campaign provenance. + return get_database_view( + "b200_sxm", + "sglang", + "0.5.17", + systems_paths=str(root), + database_mode=mode, + shared_layer=shared_layer, + strict_provenance=False, + ) + + +def _query(db, *, source=None, **overrides): + fields = { + "name": "eligibility fixture", + "scale_factor": 1.0, + "hidden_size": 8192, + "inter_size": 2048, + "topk": 8, + "num_experts": 256, + "moe_tp_size": 1, + "moe_ep_size": 1, + "attention_dp_size": 1, + "quant_mode": "fp8_block", + "workload_distribution": "uniform", + "is_gated": True, + "moe_backend": None, + "moe_kernel_source": source, + "enable_eplb": False, + "is_context": False, + **overrides, + } + return EngineHandle.for_database(db, systems_path=db.systems_root).evaluate_ops_json( + json.dumps([{"Moe": fields}]), + is_context=False, + batch_size=1, + s=1, + x=32, + )[0] + + +def test_opt_in_only_table_requires_exact_source(systems_root): + _write(systems_root, [_row(default_eligible=False)]) + db = _database(systems_root) + + # Synthetic measured latency: the named lane must remain usable even + # when the table has no automatic-selection rows at all. + assert _query(db, source=SOURCE)[1] == 0.25 + with pytest.raises(PerfDataNotAvailableError): + _query(db) + + +def test_default_views_exclude_opt_in_rows(systems_root): + rows = [_row(default_eligible=False)] + path = _write(systems_root, rows) + db = _database(systems_root) + + assert fetch_table_view(db, "_moe_data") == {} + assert db.supported_quant_mode["moe"] == [] + assert db.legacy_moe_compute_coverage(8192, 2048, 8, 256, MoEQuantMode.fp8_block) == set() + # Raw parquet enumeration keeps the exact source, metadata and measurement. + assert pq.read_table(path).to_pylist() == rows + + +@pytest.mark.parametrize("flag", [None, True, False]) +@pytest.mark.parametrize("quant", ["fp8_block", "nvfp4"]) +def test_absent_and_true_keep_legacy_default_sources(systems_root, flag, quant): + row = _row(moe_dtype=quant) + if flag is not None: + row["default_eligible"] = flag + _write(systems_root, [row]) + db = _database(systems_root) + + assert _query(db, source=SOURCE, quant_mode=quant)[1] == 0.25 + if flag is False: + with pytest.raises(PerfDataNotAvailableError): + _query(db, quant_mode=quant) + else: + assert _query(db, quant_mode=quant)[1] == 0.25 + + +@pytest.mark.parametrize("mode", ["EMPIRICAL", "HYBRID"]) +@pytest.mark.parametrize("overrides", [{}, {"hidden_size": 4096}, {"quant_mode": "bfloat16"}]) +def test_empirical_transfers_cannot_use_opt_in_rows(systems_root, mode, overrides): + _write(systems_root, [_row(default_eligible=False)]) + db = _database(systems_root, mode=mode) + + assert _query(db, source=SOURCE)[1] == pytest.approx(0.25) + with pytest.raises(EmpiricalNotImplementedError): + _query(db, **overrides) + + +@pytest.mark.parametrize("mode", ["SILICON", "EMPIRICAL", "HYBRID"]) +@pytest.mark.parametrize("flag", [None, "false", 0, 1.0]) +def test_malformed_eligibility_fails_closed(systems_root, mode, flag): + _write(systems_root, [_row(default_eligible=flag)]) + db = _database(systems_root, mode=mode) + + # InvalidPerfData crosses the FFI as ValueError, never a coverage miss. + for source in (None, SOURCE): + with pytest.raises(ValueError, match="default_eligible.*non-null Boolean"): + _query(db, source=source) + with pytest.raises(ValueError, match="default_eligible.*non-null Boolean"): + fetch_table_view(db, "_moe_data") + + +@pytest.mark.parametrize("source", [None, "", " \t", 7]) +def test_opt_in_requires_an_exact_nonblank_source(systems_root, source): + _write(systems_root, [_row(default_eligible=False, kernel_source=source)]) + db = _database(systems_root) + with pytest.raises(ValueError, match="default_eligible=false.*kernel_source"): + _query(db) + + +def test_opt_in_requires_a_source_column(systems_root): + row = _row(default_eligible=False) + del row["kernel_source"] + _write(systems_root, [row]) + with pytest.raises(ValueError, match="default_eligible=false.*kernel_source"): + _query(_database(systems_root)) + + +@pytest.mark.parametrize("mode", ["SILICON", "EMPIRICAL", "HYBRID"]) +@pytest.mark.parametrize("eligible", [False, True]) +def test_low_latency_eligibility_keeps_the_named_lane(systems_root, mode, eligible): + low_latency = "moe_torch_flow_min_latency" + _write( + systems_root, + [ + _row(moe_dtype="nvfp4", kernel_source=low_latency, default_eligible=eligible, latency=0.125), + _row(moe_dtype="nvfp4", kernel_source="moe_torch_flow", default_eligible=True), + ], + ) + db = _database(systems_root, mode=mode) + assert _query(db, source=low_latency, quant_mode="nvfp4")[1] == pytest.approx(0.125) + assert _query(db, quant_mode="nvfp4")[1] == pytest.approx(0.125 if eligible else 0.25) + assert bool(fetch_table_view(db, "_moe_low_latency_data")) is eligible + + +@pytest.mark.parametrize("shared", [False, True]) +def test_eligibility_preserves_source_priority(systems_root, shared): + _write( + systems_root, + [ + _row(default_eligible=False, latency=0.125), + _row(kernel_source="triton", default_eligible=True, moe_tp_size=2), + # Two eligible sources may legitimately own disjoint keys. + _row(kernel_source="other", default_eligible=True, moe_tp_size=4), + ], + ) + _write( + systems_root, + [ + _row(default_eligible=False, latency=0.1), + _row(kernel_source="triton", default_eligible=True, latency=0.5), + _row(kernel_source="triton", default_eligible=True, latency=0.75, moe_tp_size=2), + ], + version="0.5.16", + ) + db = _database(systems_root, shared_layer=shared) + assert _query(db, source=SOURCE)[1] == 0.125 + assert _query(db, moe_tp_size=2)[1] == 0.25 + assert _query(db, moe_tp_size=4)[1] == 0.25 + if shared: + assert _query(db)[1] == 0.5 + else: + with pytest.raises(PerfDataNotAvailableError): + _query(db) + + +def test_opt_in_source_identity_is_not_trimmed(systems_root): + source = f" {SOURCE} " + _write(systems_root, [_row(default_eligible=False, kernel_source=source)]) + db = _database(systems_root) + assert _query(db, source=source)[1] == 0.25 + with pytest.raises(PerfDataNotAvailableError): + _query(db, source=SOURCE) + + +def test_shared_cross_backend_opt_in_rows_remain_named_only(systems_root): + _write(systems_root, [_row(kernel_source="triton", default_eligible=True, moe_tp_size=2)]) + _write( + systems_root, + [ + _row(default_eligible=False), + _row(kernel_source="not_shared", default_eligible=True), + ], + backend="vllm", + version="0.24.0", + ) + (systems_root / "perf_data_reuse_manifest.yaml").write_text( + "groups:\n - op_file: moe_perf.txt\n" + f" kernel_source: {SOURCE}\n tier: shared\n frameworks: [sglang, vllm]\n" + ) + db = _database(systems_root, shared_layer=True) + assert _query(db, source=SOURCE)[1] == 0.25 + with pytest.raises(PerfDataNotAvailableError): + _query(db) + assert db.legacy_moe_compute_coverage(8192, 2048, 8, 256, MoEQuantMode.fp8_block) == set() + + +@pytest.mark.parametrize("mode", ["SILICON", "EMPIRICAL", "HYBRID"]) +def test_later_null_flag_invalidates_the_table(systems_root, mode): + _write(systems_root, [_row(default_eligible=True), _row(default_eligible=None, num_tokens=64)]) + db = _database(systems_root, mode=mode) + for _ in range(2): + with pytest.raises(ValueError, match="default_eligible.*non-null Boolean"): + _query(db) + + +@pytest.mark.parametrize("eligible", [None, True]) +def test_legacy_unnamed_default_rows_remain_eligible(systems_root, eligible): + row = _row() + del row["kernel_source"] + if eligible is not None: + row["default_eligible"] = eligible + _write(systems_root, [row]) + assert _query(_database(systems_root))[1] == 0.25 diff --git a/python/aisimulate/tests/unit/tools/test_check_cross_backend.py b/python/aisimulate/tests/unit/tools/test_check_cross_backend.py index d1d4169522..c6f1612099 100644 --- a/python/aisimulate/tests/unit/tools/test_check_cross_backend.py +++ b/python/aisimulate/tests/unit/tools/test_check_cross_backend.py @@ -62,6 +62,20 @@ def test_nan_kernel_source_nonpositive_rows_are_counted(tmp_path): assert table is not None +def test_selection_metadata_does_not_split_raw_measured_shapes(tmp_path): + path = _write( + tmp_path, + "framework,version,kernel_source,op_name,latency,num_tokens,default_eligible", + ["sglang,1.0,default,moe,1.0,32,true", "sglang,1.0,exact,moe,0.25,32,false"], + ) + table, _, _ = _load_op_table(path, "sglang", "1.0") + assert table.shape_cols == ["op_name", "num_tokens"] + # This raw measurement report includes opt-in kernels. Eligibility only + # restricts automatic prediction and its default coverage views. + assert table.frame["latency"].tolist() == [0.25] + assert table.kernel_sources == ["default", "exact"] + + def test_component_latency_is_summed_like_the_consumer(tmp_path): """wideep_deepep_ll stores combine+dispatch; the runtime consumer sums them, so the checker must audit the sum (and derived bandwidth columns diff --git a/python/aisimulate/tests/unit/tools/test_parquet_diff.py b/python/aisimulate/tests/unit/tools/test_parquet_diff.py index 3ba393572a..7f23908764 100644 --- a/python/aisimulate/tests/unit/tools/test_parquet_diff.py +++ b/python/aisimulate/tests/unit/tools/test_parquet_diff.py @@ -63,6 +63,16 @@ def test_legacy_perf_policy_allows_text_file_deletions(parquet_diff_module): assert "- Legacy `*_perf.txt` files added or modified: 0" in report +def test_moe_eligibility_change_is_metadata_on_the_same_measurement(parquet_diff_module, tmp_path): + row = {"kernel_source": "exact", "moe_dtype": "fp8_block", "num_tokens": 32, "latency": 0.25} + base = _snapshot(parquet_diff_module, "moe_perf.parquet", [{**row, "default_eligible": True}]) + head = _snapshot(parquet_diff_module, "moe_perf.parquet", [{**row, "default_eligible": False}]) + diff = parquet_diff_module._diff_snapshots("moe_perf.parquet", base, head, detail_dir=tmp_path) + assert diff.added_rows == diff.removed_rows == 0 + assert diff.modified_rows == 1 + assert "default_eligible" not in diff.key_columns + + def test_row_diff_writes_added_removed_and_modified_artifacts(parquet_diff_module, tmp_path): base = _snapshot( parquet_diff_module, diff --git a/python/aisimulate/tools/perf_database/parquet_diff.py b/python/aisimulate/tools/perf_database/parquet_diff.py index 7229159a7c..8b186f4545 100644 --- a/python/aisimulate/tools/perf_database/parquet_diff.py +++ b/python/aisimulate/tools/perf_database/parquet_diff.py @@ -198,7 +198,9 @@ def _merge_columns(*column_lists: list[str]) -> list[str]: def _select_key_columns(base_columns: list[str], head_columns: list[str]) -> list[str]: common_columns = [column for column in base_columns if column in head_columns] - key_columns = [column for column in common_columns if column not in MEASUREMENT_COLUMNS] + key_columns = [ + column for column in common_columns if column not in MEASUREMENT_COLUMNS and column != "default_eligible" + ] return key_columns diff --git a/python/aisimulate/tools/perf_database/perf_data_layout.py b/python/aisimulate/tools/perf_database/perf_data_layout.py index 64040e09a3..f1ba68cee1 100644 --- a/python/aisimulate/tools/perf_database/perf_data_layout.py +++ b/python/aisimulate/tools/perf_database/perf_data_layout.py @@ -12,7 +12,7 @@ from pathlib import Path # Columns that never participate in a perf-table shape key. -META_COLUMNS = frozenset({"framework", "version", "device", "op_name", "kernel_source"}) +META_COLUMNS = frozenset({"framework", "version", "device", "op_name", "kernel_source", "default_eligible"}) # Files to skip entirely (markers, already-shared layers, irregular formats). # reuse.yaml/collection_meta.yaml never match the *.parquet/*.txt glob below; From 6cc86fc72861bb2bac4fe8b59201b3d768000521 Mon Sep 17 00:00:00 2001 From: Yiming Liu Date: Thu, 24 Sep 2026 17:14:54 +0800 Subject: [PATCH 11/14] fix: treat null replay MoE source aliases as unset Discard null MoE source aliases before duplicate detection so optional selectors behave like omission in AFD companions and replay. Preserve non-null values, explicit-source rejection, and other timing-field validation. Add public-consumer regression coverage with synthetic timings and recording-runtime transport checks. Refs AIC-1781. Signed-off-by: Yiming Liu --- python/aisimulate/src/aisimulate/runner.py | 4 ++ tests/test_afd_runner.py | 68 ++++++++++++++++++++-- tests/test_runner.py | 42 +++++++++++++ 3 files changed, 109 insertions(+), 5 deletions(-) diff --git a/python/aisimulate/src/aisimulate/runner.py b/python/aisimulate/src/aisimulate/runner.py index 20a28a8a1f..ddc8862971 100644 --- a/python/aisimulate/src/aisimulate/runner.py +++ b/python/aisimulate/src/aisimulate/runner.py @@ -1154,6 +1154,10 @@ def _configured_in_flight_cap(spec: ReplaySpec) -> int | None: def _pop_aic_timing_overrides(rank: dict[str, JSONValue], role: str) -> dict[str, JSONValue]: aic_timing_overrides: dict[str, JSONValue] = {} for target, aliases in _AIC_TIMING_FIELD_ALIASES.items(): + if target == "moe_kernel_source": + for alias in aliases: + if rank.get(alias) is None: + rank.pop(alias, None) configured = [alias for alias in aliases if alias in rank] if len(configured) > 1: names = ", ".join(configured) diff --git a/tests/test_afd_runner.py b/tests/test_afd_runner.py index 21526af312..c1c86dd154 100644 --- a/tests/test_afd_runner.py +++ b/tests/test_afd_runner.py @@ -339,10 +339,41 @@ def test_default_companion_model_consumes_fixed_timing_without_aic_lookup(overri assert timing.provenance["provider"] == "fixed" +@pytest.mark.parametrize(("phase", "companion_role"), [("decode", "prefill"), ("prefill", "decode")]) +@pytest.mark.parametrize( + "overrides", + [ + {"moe_kernel_source": None}, + {"aic_moe_kernel_source": None}, + {"moe_kernel_source": None, "aic_moe_kernel_source": None}, + ], +) +def test_aic_companion_treats_null_moe_source_as_unset(phase, companion_role, overrides): + spec = _spec(_topology(phase=phase, combined_with_pd=True), companion_role=companion_role) + engine_args = getattr(spec.backend_deployment, f"{companion_role}_engine_args") + engine_args.pop("timing_model") + engine_args.update(aic_model_path="test-model", aic_system="test-system") + + def estimator(*args, **kwargs): + # Synthetic timing isolates optional-input handling, not prediction accuracy. + return SimpleNamespace(raw={"ttft": 2.0, "tpot": 3.0}) + + companion = AICAFDCompanionPerformanceModel(estimator) + baseline = companion.measure(spec) + runner = EngineReplayRunnerFactory(afd_companion_model=companion).create(0) + baseline_report = runner.run(spec) + engine_args.update(overrides) + + assert companion.measure(spec) == baseline + assert runner.run(spec).metrics == baseline_report.metrics + + @pytest.mark.parametrize(("phase", "companion_role"), [("decode", "prefill"), ("prefill", "decode")]) @pytest.mark.parametrize("field", ["moe_kernel_source", "aic_moe_kernel_source"]) -@pytest.mark.parametrize("custom_timing", [False, True]) -def test_aic_companion_legacy_estimator_rejects_an_explicit_moe_source(phase, companion_role, field, custom_timing): +@pytest.mark.parametrize(("custom_timing", "null_other_alias"), [(False, False), (True, False), (False, True)]) +def test_aic_companion_legacy_estimator_rejects_an_explicit_moe_source( + phase, companion_role, field, custom_timing, null_other_alias +): spec = _spec(_topology(phase=phase, combined_with_pd=True), companion_role=companion_role) engine_args = getattr(spec.backend_deployment, f"{companion_role}_engine_args") engine_args.pop("timing_model") @@ -350,6 +381,9 @@ def test_aic_companion_legacy_estimator_rejects_an_explicit_moe_source(phase, co engine_args["timing_model"] = {"type": "polynomial"} engine_args.update(aic_model_path="Qwen/Qwen3-30B-A3B", aic_system="b200_sxm") engine_args[field] = "sglang_flashinfer_trtllm_moe" + if null_other_alias: + other_field = "aic_moe_kernel_source" if field == "moe_kernel_source" else "moe_kernel_source" + engine_args[other_field] = None def estimator(*args, **kwargs): pytest.fail("an explicit source must not reach an estimator that cannot consume it") @@ -436,13 +470,19 @@ def estimator(*args, **kwargs): @pytest.mark.parametrize(("phase", "companion_role"), [("decode", "prefill"), ("prefill", "decode")]) -@pytest.mark.parametrize("field", ["forward_model", "fpm_parquet_path"]) -def test_aic_companion_rejects_duplicate_selection_aliases(phase, companion_role, field): +@pytest.mark.parametrize( + ("field", "value"), + [ + ("forward_model", "fpm"), + ("fpm_parquet_path", "/data/reviewed-fpm.parquet"), + ("moe_kernel_source", "sglang_flashinfer_trtllm_moe"), + ], +) +def test_aic_companion_rejects_duplicate_selection_aliases(phase, companion_role, field, value): spec = _spec(_topology(phase=phase, combined_with_pd=True), companion_role=companion_role) engine_args = getattr(spec.backend_deployment, f"{companion_role}_engine_args") engine_args.pop("timing_model") engine_args.update(aic_model_path="test-model", aic_system="test-system") - value = "fpm" if field == "forward_model" else "/data/reviewed-fpm.parquet" engine_args.update({field: value, f"aic_{field}": value}) def estimator(*args, **kwargs): @@ -452,6 +492,24 @@ def estimator(*args, **kwargs): AICAFDCompanionPerformanceModel(estimator).measure(spec) +@pytest.mark.parametrize("field", ["moe_kernel_source", "aic_moe_kernel_source"]) +@pytest.mark.parametrize("value", ["", False, 0, {}]) +def test_aic_companion_rejects_malformed_moe_source_with_null_alias(field, value): + spec = _spec(_topology(phase="prefill", combined_with_pd=True), companion_role="decode") + engine_args = spec.backend_deployment.decode_engine_args + engine_args.pop("timing_model") + engine_args.update( + aic_model_path="test-model", aic_system="test-system", moe_kernel_source=None, aic_moe_kernel_source=None + ) + engine_args[field] = value + + def estimator(*args, **kwargs): + pytest.fail("a malformed source must not reach estimation") + + with pytest.raises(ValueError, match="moe_kernel_source must be a string"): + AICAFDCompanionPerformanceModel(estimator).measure(spec) + + @pytest.mark.parametrize(("phase", "companion_role"), [("decode", "prefill"), ("prefill", "decode")]) @pytest.mark.parametrize("external_fpm", [False, True]) @pytest.mark.parametrize( diff --git a/tests/test_runner.py b/tests/test_runner.py index e63d8c8d34..734e1cd123 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -1324,6 +1324,48 @@ def test_runner_rejects_nested_backend_that_conflicts_with_deployment(): EngineReplayRunnerFactory(runtime=RecordingRuntime()).create(0).run(_spec(deployment=deployment)) +@pytest.mark.parametrize( + ("overrides", "expected_source"), + [ + ({}, None), + ({"moe_kernel_source": None}, None), + ({"aic_moe_kernel_source": None}, None), + ({"moe_kernel_source": None, "aic_moe_kernel_source": None}, None), + ( + {"moe_kernel_source": None, "aic_moe_kernel_source": "sglang_flashinfer_trtllm_moe"}, + "sglang_flashinfer_trtllm_moe", + ), + ( + {"moe_kernel_source": " sglang_flashinfer_trtllm_moe ", "aic_moe_kernel_source": None}, + " sglang_flashinfer_trtllm_moe ", + ), + ], +) +def test_runner_normalizes_optional_moe_source_aliases(overrides, expected_source): + # The recording runtime verifies exact configuration transport, not prediction accuracy. + runtime = RecordingRuntime() + engine_args = _engine_args(backend="sglang") + engine_args.pop("timing_model") + engine_args.update(overrides) + deployment = BackendDeploymentSpec( + deployment_mode="agg", + backend="sglang", + backend_version="0.5.17", + agg_engine_args=engine_args, + num_workers=2, + ) + + EngineReplayRunnerFactory(runtime=runtime).create(0).run(_spec(deployment=deployment)) + + rank = runtime.execution_spec["engine"]["rank"] + config = rank["timing_model"]["config"] + assert config.get("moe_kernel_source") == expected_source + assert ("moe_kernel_source" in config) == (expected_source is not None) + assert "moe_kernel_source" not in rank + assert "aic_moe_kernel_source" not in rank + assert "aic_moe_kernel_source" not in config + + def test_runner_threads_forward_model_alias_into_aic_timing(): runtime = RecordingRuntime() engine_args = _engine_args() From 3acfa103521cfdd9bc5e33e73c65ec8bb409e584 Mon Sep 17 00:00:00 2001 From: Yiming Liu Date: Thu, 24 Sep 2026 17:17:13 +0800 Subject: [PATCH 12/14] test: cover MoE eligibility in reuse manifest scan Signed-off-by: Yiming Liu --- ...nerate_perf_data_reuse_manifest_latency.py | 49 ++++++++++++++++++- .../perf_database/check_cross_backend.py | 7 +-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/python/aisimulate/tests/unit/tools/test_generate_perf_data_reuse_manifest_latency.py b/python/aisimulate/tests/unit/tools/test_generate_perf_data_reuse_manifest_latency.py index 6be19dddc2..fcefd0e516 100644 --- a/python/aisimulate/tests/unit/tools/test_generate_perf_data_reuse_manifest_latency.py +++ b/python/aisimulate/tests/unit/tools/test_generate_perf_data_reuse_manifest_latency.py @@ -3,7 +3,7 @@ """The manifest scan must derive a reproducible latency metric for every perf table. -Two properties are covered: +Three properties are covered: * Latency composition — the DeepEP dispatch/combine tables have no single `latency` column: the SDK sums component columns at load time @@ -13,11 +13,15 @@ recognised latency column. * Merge order — `record_row` is last-write-wins per (shape, framework), so the merge must walk files in a fixed order or identical runs disagree. +* Shape identity — automatic-selection eligibility is metadata, not a shape + dimension, and opt-in measurements remain available for manifest analysis. """ import sys from pathlib import Path +import pyarrow as pa +import pyarrow.parquet as pq import pytest sys.path.insert(0, str(Path(__file__).resolve().parents[3] / "tools" / "perf_database")) @@ -161,6 +165,49 @@ def test_scan_one_file_records_composed_latency(tmp_path): assert latency == pytest.approx(33.0) +def test_scan_eligibility_metadata_preserves_shape_identity(tmp_path): + """Absent, default, and opt-in eligibility describe the same physical shape.""" + shape = { + "moe_dtype": "fp8_block", + "num_tokens": 128, + "hidden_size": 7168, + "inter_size": 2048, + "topk": 8, + "num_experts": 256, + "moe_tp_size": 32, + "moe_ep_size": 1, + "distribution": "uniform", + } + source = "sglang_flashinfer_trtllm_moe" + for version, eligible in (("0.1", None), ("0.2", True), ("0.3", False)): + version_dir = tmp_path / "b200_sxm" / "moe" / "sglang" / version + version_dir.mkdir(parents=True) + row = { + "framework": "sglang", + "version": version, + "device": "b200_sxm", + "op_name": "moe", + "kernel_source": source, + **shape, + "latency": 0.25, + } + if eligible is not None: + row["default_eligible"] = eligible + pq.write_table(pa.Table.from_pylist([row]), version_dir / "moe_perf.parquet") + + groups = scan(tmp_path) + + assert set(groups) == {("b200_sxm", "moe_perf.parquet", source)} + (group,) = groups.values() + assert group.latency_by_shape_fw_version == { + tuple(shape.items()): {("sglang", version): 0.25 for version in ("0.1", "0.2", "0.3")} + } + summary = group.summary() + assert summary["total_raw_rows"] == 3 + assert summary["total_shape_keys"] == 1 + assert summary["within_framework_dedup_rows"] == 2 + + def test_merge_order_is_file_order_not_completion_order(tmp_path): """Two versions measuring one shape must always resolve to the later file's value. diff --git a/python/aisimulate/tools/perf_database/check_cross_backend.py b/python/aisimulate/tools/perf_database/check_cross_backend.py index c0810f475b..a8e8449cdb 100644 --- a/python/aisimulate/tools/perf_database/check_cross_backend.py +++ b/python/aisimulate/tools/perf_database/check_cross_backend.py @@ -85,9 +85,10 @@ in the pair-summary align_notes); otherwise the pair is skipped and reported as `schema_mismatch`. Shape-key convention follows perf_data_layout.py: every column that is not -a meta column ({framework, version, device, op_name, kernel_source}) and not -a latency column is part of the shape key. Sweep columns get log2-bucketed to -form the local-baseline bucket key. +a meta column ({framework, version, device, op_name, kernel_source, +default_eligible}) and not a latency column is part of the shape key. This +checker retains op_name to distinguish logical operations within one table. +Sweep columns get log2-bucketed to form the local-baseline bucket key. Usage: python3 tools/perf_database/check_cross_backend.py \\ From 8be644ba5dd79aeb1fb0e58071cf1bf51a521dfa Mon Sep 17 00:00:00 2001 From: Yiming Liu Date: Thu, 24 Sep 2026 18:39:14 +0800 Subject: [PATCH 13/14] fix: record GB200 FP8 FlashInfer backend facts Signed-off-by: Yiming Liu --- python/aisimulate/collector/op_backend_facts.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/aisimulate/collector/op_backend_facts.yaml b/python/aisimulate/collector/op_backend_facts.yaml index 0b61a9df5d..3d115f0fa8 100644 --- a/python/aisimulate/collector/op_backend_facts.yaml +++ b/python/aisimulate/collector/op_backend_facts.yaml @@ -1833,7 +1833,7 @@ ops: - {framework: "sglang", version: "0.5.17", system: "b300_sxm", op_name: "moe", moe_dtype: "fp8_block", backends: ["triton_fused_moe"], kernel_sources: ["sglang_fused_moe_triton"]} - {framework: "sglang", version: "0.5.17", system: "b300_sxm", op_name: "moe", moe_dtype: "nvfp4", backends: ["flashinfer_trtllm_gen"], kernel_sources: ["sglang_flashinfer_trtllm_moe"]} - {framework: "sglang", version: "0.5.17", system: "gb200", op_name: "moe", moe_dtype: "bfloat16", backends: ["triton_fused_moe"], kernel_sources: ["sglang_fused_moe_triton"]} - - {framework: "sglang", version: "0.5.17", system: "gb200", op_name: "moe", moe_dtype: "fp8_block", backends: ["triton_fused_moe"], kernel_sources: ["sglang_fused_moe_triton"]} + - {framework: "sglang", version: "0.5.17", system: "gb200", op_name: "moe", moe_dtype: "fp8_block", backends: ["flashinfer_trtllm_gen", "triton_fused_moe"], kernel_sources: ["sglang_flashinfer_trtllm_moe", "sglang_fused_moe_triton"]} - {framework: "sglang", version: "0.5.17", system: "gb200", op_name: "moe", moe_dtype: "nvfp4", backends: ["flashinfer_trtllm_gen"], kernel_sources: ["sglang_flashinfer_trtllm_moe"]} - {framework: "sglang", version: "0.5.17", system: "gb300", op_name: "moe", moe_dtype: "bfloat16", backends: ["triton_fused_moe"], kernel_sources: ["sglang_fused_moe_triton"]} - {framework: "sglang", version: "0.5.17", system: "gb300", op_name: "moe", moe_dtype: "fp8_block", backends: ["triton_fused_moe"], kernel_sources: ["sglang_fused_moe_triton"]} From 326158665336fc6b288db7bd96753cd9777b0e66 Mon Sep 17 00:00:00 2001 From: Yiming Liu Date: Tue, 29 Sep 2026 20:44:13 +0800 Subject: [PATCH 14/14] docs: refresh complete perf-data reuse analysis Regenerate the complete report with the canonical scanner and renderer after the GB200 FlashInfer FP8 data addition. Preserve all measurements, collection metadata, backend facts, and the runtime manifest. The generated base-to-head difference is exactly 3,159 rows; other report changes catch up inherited documentation drift. Signed-off-by: Yiming Liu --- .../perf_database/perf-data-reuse-analysis.md | 799 ++++++++++-------- 1 file changed, 435 insertions(+), 364 deletions(-) diff --git a/python/aisimulate/docs/perf_database/perf-data-reuse-analysis.md b/python/aisimulate/docs/perf_database/perf-data-reuse-analysis.md index 5419056378..f907b2d169 100644 --- a/python/aisimulate/docs/perf_database/perf-data-reuse-analysis.md +++ b/python/aisimulate/docs/perf_database/perf-data-reuse-analysis.md @@ -9,11 +9,11 @@ Rows with a blank/`` kernel_source are skipped during the scan (the cur ## Headline numbers -- Total rows scanned: **12,512,965** -- Within-framework cross-version dedup-able rows: **175** (~0.0%) +- Total rows scanned: **15,891,586** +- Within-framework cross-version dedup-able rows: **8,584** (~0.1%) - Tier distribution (groups / rows): - - `shared`: 1030 groups · 12,209,324 rows - - `shared_fallback`: 52 groups · 303,641 rows + - `shared`: 1084 groups · 15,585,305 rows + - `shared_fallback`: 52 groups · 306,281 rows ## `computescale_perf.parquet` @@ -53,49 +53,53 @@ Rows with a blank/`` kernel_source are skipped during the scan (the cur | a100_sxm | `torch_flow` | shared | trtllm | trtllm:4864 | 0 / 4864 | 0 | — | — | — | | a100_sxm | `vllm_flash_attn` | shared | vllm | vllm:5457 | 0 / 5049 | 0 | — | — | — | | b200_sxm | `flashinfer` | shared | sglang | sglang:2584 | 0 / 2584 | 0 | — | — | — | -| b200_sxm | `torch_flow` | shared | trtllm | trtllm:67107 | 0 / 67107 | 0 | — | — | — | +| b200_sxm | `torch_flow` | shared | trtllm | trtllm:68535 | 0 / 68535 | 0 | — | — | — | | b200_sxm | `torch_flow_flashinfer` | shared | trtllm | trtllm:2312 | 0 / 2312 | 0 | — | — | — | | b200_sxm | `triton` | shared | sglang | sglang:9486 | 0 / 9486 | 0 | — | — | — | -| b200_sxm | `trtllm_mha` | shared | sglang | sglang:31141 | 0 / 31141 | 0 | — | — | — | +| b200_sxm | `trtllm_mha` | shared | sglang | sglang:32093 | 0 / 32093 | 0 | — | — | — | | b200_sxm | `vllm_flashinfer` | shared | vllm | vllm:35360 | 0 / 35360 | 0 | — | — | — | -| b200_sxm | `vllm_flashinfer_trtllmdecode` | shared | vllm | vllm:3684 | 0 / 3684 | 0 | — | — | — | -| b200_sxm | `vllm_flashinfer_trtllmprefill` | shared | vllm | vllm:44664 | 0 / 44664 | 0 | — | — | — | -| b200_sxm | `vllm_triton_attn` | shared | vllm | vllm:3264 | 0 / 3264 | 0 | — | — | — | +| b200_sxm | `vllm_flashinfer_flashinfertrtllmapidecode` | shared | vllm | vllm:3900 | 0 / 3900 | 0 | — | — | — | +| b200_sxm | `vllm_flashinfer_trtllmdecode` | shared | vllm | vllm:3756 | 0 / 3756 | 0 | — | — | — | +| b200_sxm | `vllm_flashinfer_trtllmprefill` | shared | vllm | vllm:92848 | 0 / 47304 | 183 | — | — | — | +| b200_sxm | `vllm_triton_attn` | shared | vllm | vllm:4896 | 0 / 3264 | 3 | — | — | — | | b300_sxm | `flashinfer` | shared | sglang | sglang:2584 | 0 / 2584 | 0 | — | — | — | | b300_sxm | `torch_flow` | shared | trtllm | trtllm:67106 | 0 / 67106 | 0 | — | — | — | | b300_sxm | `torch_flow_flashinfer` | shared | trtllm | trtllm:2312 | 0 / 2312 | 0 | — | — | — | | b300_sxm | `triton` | shared | sglang | sglang:9486 | 0 / 9486 | 0 | — | — | — | | b300_sxm | `trtllm_mha` | shared | sglang | sglang:32097 | 0 / 32097 | 0 | — | — | — | +| b300_sxm | `vllm_flashinfer_flashinfertrtllmapidecode` | shared | vllm | vllm:3900 | 0 / 3900 | 0 | — | — | — | | b300_sxm | `vllm_flashinfer_trtllmdecode` | shared | vllm | vllm:3668 | 0 / 3668 | 0 | — | — | — | -| b300_sxm | `vllm_flashinfer_trtllmprefill` | shared | vllm | vllm:44476 | 0 / 44476 | 0 | — | — | — | -| b300_sxm | `vllm_triton_attn` | shared | vllm | vllm:1632 | 0 / 1632 | 0 | — | — | — | +| b300_sxm | `vllm_flashinfer_trtllmprefill` | shared | vllm | vllm:91780 | 0 / 47304 | 178 | — | — | — | +| b300_sxm | `vllm_triton_attn` | shared | vllm | vllm:3264 | 0 / 1632 | 7 | — | — | — | | b60 | `vllm_flash_attn` | shared | vllm | vllm:34118 | 0 / 17930 | 1 | — | — | — | | gb200 | `flashinfer` | shared | sglang | sglang:2584 | 0 / 2584 | 0 | — | — | — | -| gb200 | `torch_flow` | shared | trtllm | trtllm:67107 | 0 / 67107 | 0 | — | — | — | +| gb200 | `torch_flow` | shared | trtllm | trtllm:68535 | 0 / 68535 | 0 | — | — | — | | gb200 | `torch_flow_flashinfer` | shared | trtllm | trtllm:2312 | 0 / 2312 | 0 | — | — | — | | gb200 | `triton` | shared | sglang | sglang:9486 | 0 / 9486 | 0 | — | — | — | -| gb200 | `trtllm_mha` | shared | sglang | sglang:30186 | 0 / 30186 | 0 | — | — | — | -| gb200 | `vllm_flashinfer_trtllmdecode` | shared | vllm | vllm:3668 | 0 / 3668 | 0 | — | — | — | -| gb200 | `vllm_flashinfer_trtllmprefill` | shared | vllm | vllm:44476 | 0 / 44476 | 0 | — | — | — | -| gb200 | `vllm_triton_attn` | shared | vllm | vllm:1632 | 0 / 1632 | 0 | — | — | — | +| gb200 | `trtllm_mha` | shared | sglang | sglang:31138 | 0 / 31138 | 0 | — | — | — | +| gb200 | `vllm_flashinfer_flashinfertrtllmapidecode` | shared | vllm | vllm:3900 | 0 / 3900 | 0 | — | — | — | +| gb200 | `vllm_flashinfer_trtllmdecode` | shared | vllm | vllm:3740 | 0 / 3740 | 0 | — | — | — | +| gb200 | `vllm_flashinfer_trtllmprefill` | shared | vllm | vllm:92660 | 0 / 47304 | 124 | — | — | — | +| gb200 | `vllm_triton_attn` | shared | vllm | vllm:3264 | 0 / 1632 | 3 | — | — | — | | gb300 | `flashinfer` | shared | sglang | sglang:2584 | 0 / 2584 | 0 | — | — | — | -| gb300 | `torch_flow` | shared | trtllm | trtllm:67105 | 0 / 67105 | 0 | — | — | — | +| gb300 | `torch_flow` | shared | trtllm | trtllm:68533 | 0 / 68533 | 0 | — | — | — | | gb300 | `torch_flow_flashinfer` | shared | trtllm | trtllm:2312 | 0 / 2312 | 0 | — | — | — | | gb300 | `triton` | shared | sglang | sglang:9486 | 0 / 9486 | 0 | — | — | — | -| gb300 | `trtllm_mha` | shared | sglang | sglang:30192 | 0 / 30192 | 0 | — | — | — | -| gb300 | `vllm_flashinfer_trtllmdecode` | shared | vllm | vllm:3668 | 0 / 3668 | 0 | — | — | — | -| gb300 | `vllm_flashinfer_trtllmprefill` | shared | vllm | vllm:44476 | 0 / 44476 | 0 | — | — | — | -| gb300 | `vllm_triton_attn` | shared | vllm | vllm:1632 | 0 / 1632 | 0 | — | — | — | +| gb300 | `trtllm_mha` | shared | sglang | sglang:31144 | 0 / 31144 | 0 | — | — | — | +| gb300 | `vllm_flashinfer_flashinfertrtllmapidecode` | shared | vllm | vllm:3900 | 0 / 3900 | 0 | — | — | — | +| gb300 | `vllm_flashinfer_trtllmdecode` | shared | vllm | vllm:3740 | 0 / 3740 | 0 | — | — | — | +| gb300 | `vllm_flashinfer_trtllmprefill` | shared | vllm | vllm:92660 | 0 / 47304 | 207 | — | — | — | +| gb300 | `vllm_triton_attn` | shared | vllm | vllm:3264 | 0 / 1632 | 4 | — | — | — | | h100_sxm | `fa3` | shared | sglang | sglang:31622 | 0 / 31622 | 0 | — | — | — | | h100_sxm | `torch_flow` | shared | trtllm | trtllm:75840 | 0 / 75840 | 0 | — | — | — | | h100_sxm | `triton` | shared | sglang | sglang:3468 | 0 / 3468 | 0 | — | — | — | -| h100_sxm | `vllm_flash_attn_fa3` | shared | vllm | vllm:50252 | 0 / 50252 | 0 | — | — | — | -| h100_sxm | `vllm_flash_attn_fa4` | shared | vllm | vllm:578 | 0 / 578 | 0 | — | — | — | -| h200_sxm | `fa3` | shared | sglang | sglang:31622 | 0 / 31622 | 0 | — | — | — | -| h200_sxm | `torch_flow` | shared | trtllm | trtllm:75837 | 0 / 75837 | 0 | — | — | — | +| h100_sxm | `vllm_flash_attn_fa3` | shared | vllm | vllm:103326 | 0 / 53074 | 12 | — | — | — | +| h100_sxm | `vllm_flash_attn_fa4` | shared | vllm | vllm:1275 | 0 / 697 | 1 | — | — | — | +| h200_sxm | `fa3` | shared | sglang | sglang:32574 | 0 / 32574 | 0 | — | — | — | +| h200_sxm | `torch_flow` | shared | trtllm | trtllm:77265 | 0 / 77265 | 0 | — | — | — | | h200_sxm | `triton` | shared | sglang | sglang:3468 | 0 / 3468 | 0 | — | — | — | -| h200_sxm | `vllm_flash_attn_fa3` | shared | vllm | vllm:50252 | 0 / 50252 | 0 | — | — | — | -| h200_sxm | `vllm_flash_attn_fa4` | shared | vllm | vllm:578 | 0 / 578 | 0 | — | — | — | +| h200_sxm | `vllm_flash_attn_fa3` | shared | vllm | vllm:104278 | 0 / 53074 | 155 | — | — | — | +| h200_sxm | `vllm_flash_attn_fa4` | shared | vllm | vllm:1275 | 0 / 697 | 1 | — | — | — | | l40s | `flashinfer` | shared | sglang | sglang:26236 | 0 / 26236 | 0 | — | — | — | | l40s | `torch_flow` | shared | trtllm | trtllm:75768 | 0 / 75768 | 0 | — | — | — | | l40s | `triton` | shared | sglang | sglang:9792 | 0 / 9792 | 0 | — | — | — | @@ -116,18 +120,18 @@ Rows with a blank/`` kernel_source are skipped during the scan (the cur | system | kernel_source | tier | frameworks | rows_per_fw | overlap_keys | dedup rows | median % | p95 % | max % | |---|---|---|---|---|---|---|---|---|---| | a100_sxm | `default` | shared_fallback | trtllm | trtllm:2436 | 0 / 2436 | 0 | — | — | — | -| b200_sxm | `default` | shared_fallback | trtllm | trtllm:1760 | 0 / 1760 | 0 | — | — | — | -| b200_sxm | `trtllm_mla` | shared | sglang | sglang:1760 | 0 / 1760 | 0 | — | — | — | +| b200_sxm | `default` | shared_fallback | trtllm | trtllm:3080 | 0 / 3080 | 0 | — | — | — | +| b200_sxm | `trtllm_mla` | shared | sglang | sglang:3080 | 0 / 3080 | 0 | — | — | — | | b300_sxm | `default` | shared_fallback | trtllm | trtllm:1760 | 0 / 1760 | 0 | — | — | — | -| b300_sxm | `trtllm_mla` | shared | sglang | sglang:3520 | 0 / 1760 | 0 | — | — | — | +| b300_sxm | `trtllm_mla` | shared | sglang | sglang:3080 | 0 / 3080 | 0 | — | — | — | | gb200 | `default` | shared_fallback | trtllm | trtllm:1760 | 0 / 1760 | 0 | — | — | — | | gb200 | `trtllm_mla` | shared | sglang | sglang:3520 | 0 / 1760 | 0 | — | — | — | | gb300 | `default` | shared_fallback | trtllm | trtllm:1760 | 0 / 1760 | 0 | — | — | — | | gb300 | `trtllm_mla` | shared | sglang | sglang:3520 | 0 / 1760 | 0 | — | — | — | | h100_sxm | `default` | shared_fallback | trtllm | trtllm:1760 | 0 / 1760 | 0 | — | — | — | | h100_sxm | `flash_attention` | shared | sglang | sglang:3520 | 0 / 1760 | 0 | — | — | — | -| h200_sxm | `default` | shared_fallback | trtllm | trtllm:1760 | 0 / 1760 | 0 | — | — | — | -| h200_sxm | `flash_attention` | shared | sglang | sglang:1760 | 0 / 1760 | 0 | — | — | — | +| h200_sxm | `default` | shared_fallback | trtllm | trtllm:3080 | 0 / 3080 | 0 | — | — | — | +| h200_sxm | `flash_attention` | shared | sglang | sglang:3080 | 0 / 3080 | 0 | — | — | — | | l40s | `default` | shared_fallback | trtllm | trtllm:2436 | 0 / 2436 | 0 | — | — | — | | l40s | `triton` | shared | sglang | sglang:880 | 0 / 880 | 0 | — | — | — | | rtx_pro_6000_server | `default` | shared_fallback | trtllm | trtllm:1760 | 0 / 1760 | 0 | — | — | — | @@ -144,7 +148,7 @@ Rows with a blank/`` kernel_source are skipped during the scan (the cur | a100_sxm | `vLLM_custom_graph` | shared | vllm | vllm:69 | 0 / 69 | 0 | — | — | — | | b200_sxm | `SGLang_CustomAllReduce_eager` | shared | sglang | sglang:207 | 0 / 69 | 0 | — | — | — | | b200_sxm | `SGLang_CustomAllReduce_graph` | shared | sglang | sglang:207 | 0 / 69 | 0 | — | — | — | -| b200_sxm | `TRTLLM` | shared | trtllm | trtllm:207 | 0 / 69 | 0 | — | — | — | +| b200_sxm | `TRTLLM` | shared | trtllm | trtllm:207 | 0 / 138 | 0 | — | — | — | | b200_sxm | `vLLM_custom_eager` | shared | vllm | vllm:138 | 0 / 69 | 0 | — | — | — | | b200_sxm | `vLLM_custom_graph` | shared | vllm | vllm:138 | 0 / 69 | 0 | — | — | — | | b300_sxm | `SGLang_CustomAllReduce_eager` | shared | sglang | sglang:207 | 0 / 69 | 0 | — | — | — | @@ -153,8 +157,8 @@ Rows with a blank/`` kernel_source are skipped during the scan (the cur | b300_sxm | `vLLM_custom_eager` | shared | vllm | vllm:138 | 0 / 69 | 0 | — | — | — | | b300_sxm | `vLLM_custom_graph` | shared | vllm | vllm:138 | 0 / 69 | 0 | — | — | — | | b60 | `vLLM_custom_eager` | shared | vllm | vllm:207 | 0 / 69 | 23 | — | — | — | -| gb200 | `SGLang_CustomAllReduce_eager` | shared | sglang | sglang:184 | 0 / 46 | 0 | — | — | — | -| gb200 | `SGLang_CustomAllReduce_graph` | shared | sglang | sglang:184 | 0 / 46 | 0 | — | — | — | +| gb200 | `SGLang_CustomAllReduce_eager` | shared | sglang | sglang:230 | 0 / 92 | 0 | — | — | — | +| gb200 | `SGLang_CustomAllReduce_graph` | shared | sglang | sglang:230 | 0 / 92 | 0 | — | — | — | | gb200 | `TRTLLM` | shared | trtllm | trtllm:92 | 0 / 46 | 0 | — | — | — | | gb200 | `vLLM_custom_eager` | shared | vllm | vllm:138 | 0 / 46 | 0 | — | — | — | | gb200 | `vLLM_custom_graph` | shared | vllm | vllm:138 | 0 / 46 | 0 | — | — | — | @@ -274,25 +278,50 @@ Rows with a blank/`` kernel_source are skipped during the scan (the cur | h200_sxm | `sglang_dsa_skip_indexer_fa3` | shared | sglang | sglang:1224 | 0 / 1224 | 0 | — | — | — | | h200_sxm | `sglang_dsa_skip_indexer_flashmla_kv` | shared | sglang | sglang:1224 | 0 / 1224 | 0 | — | — | — | +## `dsv41_module_perf.parquet` + +| system | kernel_source | tier | frameworks | rows_per_fw | overlap_keys | dedup rows | median % | p95 % | max % | +|---|---|---|---|---|---|---|---|---|---| +| b200_sxm | `sglang.srt.layers.engram.Engram.apply_gate+sglang.srt.layers.engram.EngramEmbedding._owned_rows+sglang.srt.layers.linear.ReplicatedLinear.forward/sglang.srt.layers.quantization.fp8.Fp8LinearMethod` | shared | sglang | sglang:60 | 0 / 60 | 0 | — | — | — | +| b200_sxm | `sglang.srt.layers.linear.MergedColumnParallelLinear.forward/sglang.srt.layers.quantization.fp8.Fp8LinearMethod` | shared | sglang | sglang:30 | 0 / 30 | 0 | — | — | — | +| b200_sxm | `sglang.srt.layers.linear.RowParallelLinear.forward/sglang.srt.layers.quantization.fp8.Fp8LinearMethod` | shared | sglang | sglang:30 | 0 / 30 | 0 | — | — | — | +| b200_sxm | `sglang.srt.models.deepseek_v4.DeepseekV4DecoderLayer._hc_mix_and_combine+sglang.srt.models.deepseek_v4.DeepseekV4DecoderLayer.hc_post` | shared | sglang | sglang:15 | 0 / 15 | 0 | — | — | — | +| b200_sxm | `sglang.srt.models.deepseek_v4.MQALayer.forward` | shared | sglang | sglang:2260 | 0 / 2260 | 0 | — | — | — | +| gb200 | `sglang.srt.layers.engram.Engram.apply_gate+sglang.srt.layers.engram.EngramEmbedding._owned_rows+sglang.srt.layers.linear.ReplicatedLinear.forward/sglang.srt.layers.quantization.fp8.Fp8LinearMethod` | shared | sglang | sglang:60 | 0 / 60 | 0 | — | — | — | +| gb200 | `sglang.srt.layers.linear.MergedColumnParallelLinear.forward/sglang.srt.layers.quantization.fp8.Fp8LinearMethod` | shared | sglang | sglang:30 | 0 / 30 | 0 | — | — | — | +| gb200 | `sglang.srt.layers.linear.RowParallelLinear.forward/sglang.srt.layers.quantization.fp8.Fp8LinearMethod` | shared | sglang | sglang:30 | 0 / 30 | 0 | — | — | — | +| gb200 | `sglang.srt.models.deepseek_v4.DeepseekV4DecoderLayer._hc_mix_and_combine+sglang.srt.models.deepseek_v4.DeepseekV4DecoderLayer.hc_post` | shared | sglang | sglang:15 | 0 / 15 | 0 | — | — | — | +| gb200 | `sglang.srt.models.deepseek_v4.MQALayer.forward` | shared | sglang | sglang:2260 | 0 / 2260 | 0 | — | — | — | +| h100_sxm | `sglang.srt.layers.engram.Engram.apply_gate+sglang.srt.layers.engram.EngramEmbedding._owned_rows+sglang.srt.layers.linear.ReplicatedLinear.forward/sglang.srt.layers.quantization.fp8.Fp8LinearMethod` | shared | sglang | sglang:60 | 0 / 60 | 0 | — | — | — | +| h100_sxm | `sglang.srt.layers.linear.MergedColumnParallelLinear.forward/sglang.srt.layers.quantization.fp8.Fp8LinearMethod` | shared | sglang | sglang:30 | 0 / 30 | 0 | — | — | — | +| h100_sxm | `sglang.srt.layers.linear.RowParallelLinear.forward/sglang.srt.layers.quantization.fp8.Fp8LinearMethod` | shared | sglang | sglang:30 | 0 / 30 | 0 | — | — | — | +| h100_sxm | `sglang.srt.models.deepseek_v4.DeepseekV4DecoderLayer._hc_mix_and_combine+sglang.srt.models.deepseek_v4.DeepseekV4DecoderLayer.hc_post` | shared | sglang | sglang:15 | 0 / 15 | 0 | — | — | — | +| h100_sxm | `sglang.srt.models.deepseek_v4.MQALayer.forward` | shared | sglang | sglang:2260 | 0 / 2260 | 0 | — | — | — | +| h200_sxm | `sglang.srt.layers.engram.Engram.apply_gate+sglang.srt.layers.engram.EngramEmbedding._owned_rows+sglang.srt.layers.linear.ReplicatedLinear.forward/sglang.srt.layers.quantization.fp8.Fp8LinearMethod` | shared | sglang | sglang:60 | 0 / 60 | 0 | — | — | — | +| h200_sxm | `sglang.srt.layers.linear.MergedColumnParallelLinear.forward/sglang.srt.layers.quantization.fp8.Fp8LinearMethod` | shared | sglang | sglang:30 | 0 / 30 | 0 | — | — | — | +| h200_sxm | `sglang.srt.layers.linear.RowParallelLinear.forward/sglang.srt.layers.quantization.fp8.Fp8LinearMethod` | shared | sglang | sglang:30 | 0 / 30 | 0 | — | — | — | +| h200_sxm | `sglang.srt.models.deepseek_v4.DeepseekV4DecoderLayer._hc_mix_and_combine+sglang.srt.models.deepseek_v4.DeepseekV4DecoderLayer.hc_post` | shared | sglang | sglang:15 | 0 / 15 | 0 | — | — | — | +| h200_sxm | `sglang.srt.models.deepseek_v4.MQALayer.forward` | shared | sglang | sglang:2260 | 0 / 2260 | 0 | — | — | — | + ## `dsv4_csa_context_module_perf.parquet` | system | kernel_source | tier | frameworks | rows_per_fw | overlap_keys | dedup rows | median % | p95 % | max % | |---|---|---|---|---|---|---|---|---|---| | b200_sxm | `DeepseekV4TrtllmAttention` | shared | trtllm | trtllm:5587 | 0 / 5587 | 0 | — | — | — | -| b200_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:5840 | 0 / 5840 | 0 | — | — | — | +| b200_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:11680 | 0 / 8760 | 20 | — | — | — | | b200_sxm | `compressed_flashmla` | shared | sglang | sglang:43804 | 0 / 43804 | 0 | — | — | — | | b300_sxm | `DeepseekV4TrtllmAttention` | shared | trtllm | trtllm:5600 | 0 / 5600 | 0 | — | — | — | -| b300_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:5704 | 0 / 5704 | 0 | — | — | — | +| b300_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:11544 | 0 / 8624 | 22 | — | — | — | | b300_sxm | `compressed_flashmla` | shared | sglang | sglang:89689 | 0 / 45449 | 0 | — | — | — | | gb200 | `DeepseekV4TrtllmAttention` | shared | trtllm | trtllm:5584 | 0 / 5584 | 0 | — | — | — | -| gb200 | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:5840 | 0 / 5840 | 0 | — | — | — | +| gb200 | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:11680 | 0 / 8760 | 9 | — | — | — | | gb200 | `compressed_flashmla` | shared | sglang | sglang:53376 | 0 / 42610 | 0 | — | — | — | | gb300 | `DeepseekV4TrtllmAttention` | shared | trtllm | trtllm:5600 | 0 / 5600 | 0 | — | — | — | -| gb300 | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:5704 | 0 / 5704 | 0 | — | — | — | +| gb300 | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:11544 | 0 / 8624 | 10 | — | — | — | | gb300 | `compressed_flashmla` | shared | sglang | sglang:94650 | 0 / 45424 | 0 | — | — | — | -| h100_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:5776 | 0 / 5776 | 0 | — | — | — | +| h100_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:11565 | 0 / 5790 | 17 | — | — | — | | h100_sxm | `compressed_flashmla` | shared | sglang | sglang:52902 | 0 / 40384 | 0 | — | — | — | -| h200_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:5840 | 0 / 5840 | 0 | — | — | — | +| h200_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:11680 | 0 / 5840 | 45 | — | — | — | | h200_sxm | `compressed_flashmla` | shared | sglang | sglang:40592 | 0 / 40592 | 0 | — | — | — | ## `dsv4_csa_generation_module_perf.parquet` @@ -300,20 +329,20 @@ Rows with a blank/`` kernel_source are skipped during the scan (the cur | system | kernel_source | tier | frameworks | rows_per_fw | overlap_keys | dedup rows | median % | p95 % | max % | |---|---|---|---|---|---|---|---|---|---| | b200_sxm | `DeepseekV4TrtllmAttention` | shared | trtllm | trtllm:1544 | 0 / 1544 | 0 | — | — | — | -| b200_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:1544 | 0 / 1544 | 0 | — | — | — | +| b200_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:3088 | 0 / 2316 | 9 | — | — | — | | b200_sxm | `compressed_flashmla` | shared | sglang | sglang:3200 | 0 / 3200 | 0 | — | — | — | | b300_sxm | `DeepseekV4TrtllmAttention` | shared | trtllm | trtllm:1544 | 0 / 1544 | 0 | — | — | — | -| b300_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:1048 | 0 / 1048 | 0 | — | — | — | +| b300_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:2592 | 0 / 1820 | 12 | — | — | — | | b300_sxm | `compressed_flashmla` | shared | sglang | sglang:3200 | 0 / 3200 | 0 | — | — | — | | gb200 | `DeepseekV4TrtllmAttention` | shared | trtllm | trtllm:1544 | 0 / 1544 | 0 | — | — | — | -| gb200 | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:1544 | 0 / 1544 | 0 | — | — | — | +| gb200 | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:3088 | 0 / 2316 | 6 | — | — | — | | gb200 | `compressed_flashmla` | shared | sglang | sglang:3200 | 0 / 3200 | 0 | — | — | — | | gb300 | `DeepseekV4TrtllmAttention` | shared | trtllm | trtllm:1544 | 0 / 1544 | 0 | — | — | — | -| gb300 | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:1048 | 0 / 1048 | 0 | — | — | — | +| gb300 | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:2592 | 0 / 1820 | 6 | — | — | — | | gb300 | `compressed_flashmla` | shared | sglang | sglang:3200 | 0 / 3200 | 0 | — | — | — | -| h100_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:1544 | 0 / 1544 | 0 | — | — | — | +| h100_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:3088 | 0 / 1544 | 10 | — | — | — | | h100_sxm | `compressed_flashmla` | shared | sglang | sglang:3200 | 0 / 3200 | 0 | — | — | — | -| h200_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:1544 | 0 / 1544 | 0 | — | — | — | +| h200_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:3088 | 0 / 1544 | 29 | — | — | — | | h200_sxm | `compressed_flashmla` | shared | sglang | sglang:3200 | 0 / 3200 | 0 | — | — | — | | rtx_pro_6000_server | `compressed_flashmla` | shared | sglang | sglang:909 | 0 / 868 | 0 | — | — | — | @@ -338,32 +367,32 @@ Rows with a blank/`` kernel_source are skipped during the scan (the cur | system | kernel_source | tier | frameworks | rows_per_fw | overlap_keys | dedup rows | median % | p95 % | max % | |---|---|---|---|---|---|---|---|---|---| -| b200_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:1959 | 0 / 1959 | 0 | — | — | — | -| b300_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:1959 | 0 / 1959 | 0 | — | — | — | -| gb200 | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:1959 | 0 / 1959 | 0 | — | — | — | -| gb300 | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:1959 | 0 / 1959 | 0 | — | — | — | -| h100_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:1959 | 0 / 1959 | 0 | — | — | — | -| h200_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:1959 | 0 / 1959 | 0 | — | — | — | +| b200_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:5877 | 0 / 3918 | 155 | — | — | — | +| b300_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:5877 | 0 / 3918 | 112 | — | — | — | +| gb200 | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:5877 | 0 / 3918 | 116 | — | — | — | +| gb300 | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:5877 | 0 / 3918 | 90 | — | — | — | +| h100_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:5877 | 0 / 3918 | 42 | — | — | — | +| h200_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:5877 | 0 / 3918 | 128 | — | — | — | ## `dsv4_hca_context_module_perf.parquet` | system | kernel_source | tier | frameworks | rows_per_fw | overlap_keys | dedup rows | median % | p95 % | max % | |---|---|---|---|---|---|---|---|---|---| | b200_sxm | `DeepseekV4TrtllmAttention` | shared | trtllm | trtllm:5589 | 0 / 5589 | 0 | — | — | — | -| b200_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:5840 | 0 / 5840 | 0 | — | — | — | +| b200_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:11680 | 0 / 8760 | 17 | — | — | — | | b200_sxm | `compressed_flashmla` | shared | sglang | sglang:47456 | 0 / 47456 | 0 | — | — | — | | b300_sxm | `DeepseekV4TrtllmAttention` | shared | trtllm | trtllm:5600 | 0 / 5600 | 0 | — | — | — | -| b300_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:5840 | 0 / 5840 | 0 | — | — | — | +| b300_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:11680 | 0 / 8760 | 43 | — | — | — | | b300_sxm | `compressed_flashmla` | shared | sglang | sglang:61527 | 0 / 47550 | 0 | — | — | — | | gb200 | `DeepseekV4TrtllmAttention` | shared | trtllm | trtllm:5584 | 0 / 5584 | 0 | — | — | — | -| gb200 | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:5840 | 0 / 5840 | 0 | — | — | — | +| gb200 | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:11680 | 0 / 8760 | 15 | — | — | — | | gb200 | `compressed_flashmla` | shared | sglang | sglang:47456 | 0 / 47456 | 0 | — | — | — | | gb300 | `DeepseekV4TrtllmAttention` | shared | trtllm | trtllm:5600 | 0 / 5600 | 0 | — | — | — | -| gb300 | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:5840 | 0 / 5840 | 0 | — | — | — | +| gb300 | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:11680 | 0 / 8760 | 10 | — | — | — | | gb300 | `compressed_flashmla` | shared | sglang | sglang:54924 | 0 / 47552 | 0 | — | — | — | -| h100_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:5793 | 0 / 5793 | 0 | — | — | — | +| h100_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:11586 | 0 / 5793 | 42 | — | — | — | | h100_sxm | `compressed_flashmla` | shared | sglang | sglang:40383 | 0 / 40383 | 0 | — | — | — | -| h200_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:5840 | 0 / 5840 | 0 | — | — | — | +| h200_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:11680 | 0 / 5840 | 65 | — | — | — | | h200_sxm | `compressed_flashmla` | shared | sglang | sglang:40592 | 0 / 40592 | 0 | — | — | — | | rtx_pro_6000_server | `compressed_flashmla` | shared | sglang | sglang:40384 | 0 / 20192 | 0 | — | — | — | @@ -372,20 +401,20 @@ Rows with a blank/`` kernel_source are skipped during the scan (the cur | system | kernel_source | tier | frameworks | rows_per_fw | overlap_keys | dedup rows | median % | p95 % | max % | |---|---|---|---|---|---|---|---|---|---| | b200_sxm | `DeepseekV4TrtllmAttention` | shared | trtllm | trtllm:1544 | 0 / 1544 | 0 | — | — | — | -| b200_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:1544 | 0 / 1544 | 0 | — | — | — | +| b200_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:3088 | 0 / 2316 | 10 | — | — | — | | b200_sxm | `compressed_flashmla` | shared | sglang | sglang:3200 | 0 / 3200 | 0 | — | — | — | | b300_sxm | `DeepseekV4TrtllmAttention` | shared | trtllm | trtllm:1544 | 0 / 1544 | 0 | — | — | — | -| b300_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:1544 | 0 / 1544 | 0 | — | — | — | +| b300_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:3088 | 0 / 2316 | 20 | — | — | — | | b300_sxm | `compressed_flashmla` | shared | sglang | sglang:3200 | 0 / 3200 | 0 | — | — | — | | gb200 | `DeepseekV4TrtllmAttention` | shared | trtllm | trtllm:1544 | 0 / 1544 | 0 | — | — | — | -| gb200 | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:1544 | 0 / 1544 | 0 | — | — | — | +| gb200 | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:3088 | 0 / 2316 | 6 | — | — | — | | gb200 | `compressed_flashmla` | shared | sglang | sglang:3200 | 0 / 3200 | 0 | — | — | — | | gb300 | `DeepseekV4TrtllmAttention` | shared | trtllm | trtllm:1544 | 0 / 1544 | 0 | — | — | — | -| gb300 | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:1544 | 0 / 1544 | 0 | — | — | — | +| gb300 | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:3088 | 0 / 2316 | 7 | — | — | — | | gb300 | `compressed_flashmla` | shared | sglang | sglang:3200 | 0 / 3200 | 0 | — | — | — | -| h100_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:1544 | 0 / 1544 | 0 | — | — | — | +| h100_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:3088 | 0 / 1544 | 10 | — | — | — | | h100_sxm | `compressed_flashmla` | shared | sglang | sglang:3200 | 0 / 3200 | 0 | — | — | — | -| h200_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:1544 | 0 / 1544 | 0 | — | — | — | +| h200_sxm | `FLASHMLA_SPARSE_DSV4` | shared | vllm | vllm:3088 | 0 / 1544 | 47 | — | — | — | | h200_sxm | `compressed_flashmla` | shared | sglang | sglang:3200 | 0 / 3200 | 0 | — | — | — | | rtx_pro_6000_server | `compressed_flashmla` | shared | sglang | sglang:1600 | 0 / 1600 | 0 | — | — | — | @@ -402,17 +431,17 @@ Rows with a blank/`` kernel_source are skipped during the scan (the cur | system | kernel_source | tier | frameworks | rows_per_fw | overlap_keys | dedup rows | median % | p95 % | max % | |---|---|---|---|---|---|---|---|---|---| | b200_sxm | `deep_gemm.fp8_paged_mqa_logits` | shared | sglang | sglang:2181 | 0 / 2181 | 0 | — | — | — | -| b200_sxm | `vllm.utils.deep_gemm.fp8_fp4_paged_mqa_logits` | shared | vllm | vllm:1959 | 0 / 1959 | 0 | — | — | — | +| b200_sxm | `vllm.utils.deep_gemm.fp8_fp4_paged_mqa_logits` | shared | vllm | vllm:5877 | 0 / 3918 | 226 | — | — | — | | b300_sxm | `deep_gemm.fp8_paged_mqa_logits` | shared | sglang | sglang:4362 | 0 / 2181 | 0 | — | — | — | -| b300_sxm | `vllm.utils.deep_gemm.fp8_fp4_paged_mqa_logits` | shared | vllm | vllm:1959 | 0 / 1959 | 0 | — | — | — | +| b300_sxm | `vllm.utils.deep_gemm.fp8_fp4_paged_mqa_logits` | shared | vllm | vllm:5877 | 0 / 3918 | 263 | — | — | — | | gb200 | `deep_gemm.fp8_paged_mqa_logits` | shared | sglang | sglang:4261 | 0 / 2181 | 0 | — | — | — | -| gb200 | `vllm.utils.deep_gemm.fp8_fp4_paged_mqa_logits` | shared | vllm | vllm:1959 | 0 / 1959 | 0 | — | — | — | +| gb200 | `vllm.utils.deep_gemm.fp8_fp4_paged_mqa_logits` | shared | vllm | vllm:5877 | 0 / 3918 | 86 | — | — | — | | gb300 | `deep_gemm.fp8_paged_mqa_logits` | shared | sglang | sglang:4362 | 0 / 2181 | 0 | — | — | — | -| gb300 | `vllm.utils.deep_gemm.fp8_fp4_paged_mqa_logits` | shared | vllm | vllm:1959 | 0 / 1959 | 0 | — | — | — | +| gb300 | `vllm.utils.deep_gemm.fp8_fp4_paged_mqa_logits` | shared | vllm | vllm:5877 | 0 / 3918 | 149 | — | — | — | | h100_sxm | `deep_gemm.fp8_paged_mqa_logits` | shared | sglang | sglang:2055 | 0 / 2055 | 0 | — | — | — | -| h100_sxm | `vllm.utils.deep_gemm.fp8_fp4_paged_mqa_logits` | shared | vllm | vllm:1959 | 0 / 1959 | 0 | — | — | — | +| h100_sxm | `vllm.utils.deep_gemm.fp8_fp4_paged_mqa_logits` | shared | vllm | vllm:5877 | 0 / 3918 | 77 | — | — | — | | h200_sxm | `deep_gemm.fp8_paged_mqa_logits` | shared | sglang | sglang:2055 | 0 / 2055 | 0 | — | — | — | -| h200_sxm | `vllm.utils.deep_gemm.fp8_fp4_paged_mqa_logits` | shared | vllm | vllm:1959 | 0 / 1959 | 0 | — | — | — | +| h200_sxm | `vllm.utils.deep_gemm.fp8_fp4_paged_mqa_logits` | shared | vllm | vllm:5877 | 0 / 3918 | 192 | — | — | — | ## `encoder_attention_perf.parquet` @@ -420,22 +449,22 @@ Rows with a blank/`` kernel_source are skipped during the scan (the cur |---|---|---|---|---|---|---|---|---|---| | b200_sxm | `flash_attention_v4` | shared | sglang | sglang:7679 | 0 / 7679 | 0 | — | — | — | | b200_sxm | `torch_flow` | shared | trtllm | trtllm:7679 | 0 / 7679 | 0 | — | — | — | -| b200_sxm | `vllm_vit_flash_attn_fa4` | shared | vllm | vllm:7679 | 0 / 7679 | 0 | — | — | — | +| b200_sxm | `vllm_vit_flash_attn_fa4` | shared | vllm | vllm:15358 | 0 / 7679 | 32 | — | — | — | | b300_sxm | `torch_flow` | shared | trtllm | trtllm:7679 | 0 / 7679 | 0 | — | — | — | | b300_sxm | `triton` | shared | sglang | sglang:7679 | 0 / 7679 | 0 | — | — | — | -| b300_sxm | `vllm_vit_flash_attn_fa4` | shared | vllm | vllm:7679 | 0 / 7679 | 0 | — | — | — | +| b300_sxm | `vllm_vit_flash_attn_fa4` | shared | vllm | vllm:15358 | 0 / 7679 | 28 | — | — | — | | gb200 | `flash_attention_v4` | shared | sglang | sglang:7679 | 0 / 7679 | 0 | — | — | — | | gb200 | `torch_flow` | shared | trtllm | trtllm:7679 | 0 / 7679 | 0 | — | — | — | -| gb200 | `vllm_vit_flash_attn_fa4` | shared | vllm | vllm:7679 | 0 / 7679 | 0 | — | — | — | +| gb200 | `vllm_vit_flash_attn_fa4` | shared | vllm | vllm:15358 | 0 / 7679 | 26 | — | — | — | | gb300 | `torch_flow` | shared | trtllm | trtllm:7679 | 0 / 7679 | 0 | — | — | — | | gb300 | `triton` | shared | sglang | sglang:7679 | 0 / 7679 | 0 | — | — | — | -| gb300 | `vllm_vit_flash_attn_fa4` | shared | vllm | vllm:7679 | 0 / 7679 | 0 | — | — | — | +| gb300 | `vllm_vit_flash_attn_fa4` | shared | vllm | vllm:15358 | 0 / 7679 | 19 | — | — | — | | h100_sxm | `flash_attention_v3` | shared | sglang | sglang:7679 | 0 / 7679 | 0 | — | — | — | | h100_sxm | `torch_flow` | shared | trtllm | trtllm:7679 | 0 / 7679 | 0 | — | — | — | -| h100_sxm | `vllm_vit_flash_attn_fa3` | shared | vllm | vllm:7679 | 0 / 7679 | 0 | — | — | — | +| h100_sxm | `vllm_vit_flash_attn_fa3` | shared | vllm | vllm:15358 | 0 / 7679 | 5 | — | — | — | | h200_sxm | `flash_attention_v3` | shared | sglang | sglang:7679 | 0 / 7679 | 0 | — | — | — | | h200_sxm | `torch_flow` | shared | trtllm | trtllm:7679 | 0 / 7679 | 0 | — | — | — | -| h200_sxm | `vllm_vit_flash_attn_fa3` | shared | vllm | vllm:7679 | 0 / 7679 | 0 | — | — | — | +| h200_sxm | `vllm_vit_flash_attn_fa3` | shared | vllm | vllm:15358 | 0 / 7679 | 17 | — | — | — | | l40s | `torch_flow` | shared | trtllm | trtllm:7679 | 0 / 7679 | 0 | — | — | — | | l40s | `triton` | shared | sglang | sglang:7679 | 0 / 7679 | 0 | — | — | — | | l40s | `vllm_vit_flash_attn_fa2` | shared | vllm | vllm:7679 | 0 / 7679 | 0 | — | — | — | @@ -450,42 +479,46 @@ Rows with a blank/`` kernel_source are skipped during the scan (the cur | a100_sxm | `causal_conv1d_fn` | shared | sglang | sglang:824 | 0 / 824 | 0 | — | — | — | | a100_sxm | `causal_conv1d_update` | shared | sglang | sglang:88 | 0 / 88 | 0 | — | — | — | | a100_sxm | `fused_recurrent_gated_delta_rule` | shared | sglang | sglang:86 | 0 / 86 | 0 | — | — | — | -| b200_sxm | `causal_conv1d_fn` | shared | sglang, trtllm, vllm | sglang:4004, trtllm:854, vllm:4144 | 4034 / 4144 | 0 | 6.4 | 145.2 | 277.4 | -| b200_sxm | `causal_conv1d_update` | shared | sglang, trtllm, vllm | sglang:407, trtllm:88, vllm:407 | 407 / 407 | 0 | 10.8 | 77.7 | 130.9 | -| b200_sxm | `chunk_gated_delta_rule` | shared | sglang, trtllm | sglang:4004, trtllm:854 | 824 / 4034 | 0 | 18.7 | 41.1 | 45.6 | -| b200_sxm | `chunk_gated_delta_rule_flashinfer` | shared | vllm | vllm:4144 | 0 / 4144 | 0 | — | — | — | +| b200_sxm | `causal_conv1d_fn` | shared | sglang, trtllm, vllm | sglang:4529, trtllm:854, vllm:9408 | 4529 / 5558 | 8 | 6.1 | 12.0 | 28.2 | +| b200_sxm | `causal_conv1d_update` | shared | sglang, trtllm, vllm | sglang:462, trtllm:88, vllm:922 | 461 / 550 | 2 | 9.8 | 26.8 | 33.8 | +| b200_sxm | `chunk_gated_delta_rule` | shared | sglang, trtllm | sglang:4529, trtllm:854 | 0 / 5383 | 0 | — | — | — | +| b200_sxm | `chunk_gated_delta_rule_flashinfer` | shared | vllm | vllm:9408 | 0 / 4704 | 0 | — | — | — | +| b200_sxm | `flashinfer_gated_delta_rule_decode` | shared | sglang | sglang:52 | 0 / 52 | 0 | — | — | — | | b200_sxm | `fused_recurrent_gated_delta_rule` | shared | trtllm | trtllm:86 | 0 / 86 | 0 | — | — | — | -| b200_sxm | `fused_recurrent_gated_delta_rule_packed_decode` | shared | sglang, vllm | sglang:405, vllm:405 | 405 / 405 | 0 | 0.3 | 4.0 | 5.5 | -| b300_sxm | `causal_conv1d_fn` | shared | sglang, trtllm, vllm | sglang:4004, trtllm:837, vllm:4144 | 4042 / 4144 | 0 | 5.9 | 143.0 | 277.9 | -| b300_sxm | `causal_conv1d_update` | shared | sglang, trtllm, vllm | sglang:407, trtllm:88, vllm:407 | 407 / 407 | 0 | 11.6 | 75.9 | 129.9 | -| b300_sxm | `chunk_gated_delta_rule` | shared | sglang, trtllm | sglang:4004, trtllm:836 | 799 / 4041 | 0 | 31.2 | 46.0 | 50.6 | -| b300_sxm | `chunk_gated_delta_rule_flashinfer` | shared | vllm | vllm:4144 | 0 / 4144 | 0 | — | — | — | +| b200_sxm | `fused_recurrent_gated_delta_rule_packed_decode` | shared | sglang, vllm | sglang:457, vllm:914 | 457 / 457 | 3 | 0.5 | 4.5 | 9.3 | +| b300_sxm | `causal_conv1d_fn` | shared | sglang, trtllm, vllm | sglang:4529, trtllm:837, vllm:9408 | 4567 / 4704 | 3 | 8.1 | 138.5 | 277.1 | +| b300_sxm | `causal_conv1d_update` | shared | sglang, trtllm, vllm | sglang:462, trtllm:88, vllm:922 | 461 / 462 | 0 | 10.2 | 70.9 | 129.4 | +| b300_sxm | `chunk_gated_delta_rule` | shared | sglang, trtllm | sglang:4529, trtllm:836 | 799 / 4566 | 0 | 31.2 | 46.0 | 50.6 | +| b300_sxm | `chunk_gated_delta_rule_flashinfer` | shared | vllm | vllm:9408 | 0 / 4704 | 1 | — | — | — | +| b300_sxm | `flashinfer_gated_delta_rule_decode` | shared | sglang | sglang:52 | 0 / 52 | 0 | — | — | — | | b300_sxm | `fused_recurrent_gated_delta_rule` | shared | trtllm | trtllm:86 | 0 / 86 | 0 | — | — | — | -| b300_sxm | `fused_recurrent_gated_delta_rule_packed_decode` | shared | sglang, vllm | sglang:405, vllm:405 | 405 / 405 | 0 | 0.3 | 3.6 | 6.5 | -| gb200 | `causal_conv1d_fn` | shared | sglang, trtllm, vllm | sglang:4004, trtllm:854, vllm:4144 | 4034 / 4144 | 0 | 3.5 | 146.6 | 276.0 | -| gb200 | `causal_conv1d_update` | shared | sglang, trtllm, vllm | sglang:407, trtllm:88, vllm:407 | 407 / 407 | 0 | 9.4 | 112.6 | 137.5 | -| gb200 | `chunk_gated_delta_rule` | shared | sglang, trtllm | sglang:4004, trtllm:854 | 824 / 4034 | 0 | 22.8 | 54.0 | 68.0 | -| gb200 | `chunk_gated_delta_rule_flashinfer` | shared | vllm | vllm:4144 | 0 / 4144 | 0 | — | — | — | +| b300_sxm | `fused_recurrent_gated_delta_rule_packed_decode` | shared | sglang, vllm | sglang:457, vllm:914 | 457 / 457 | 0 | 1.2 | 6.3 | 9.3 | +| gb200 | `causal_conv1d_fn` | shared | sglang, trtllm, vllm | sglang:4529, trtllm:854, vllm:9408 | 4559 / 4704 | 5 | 3.5 | 137.4 | 276.6 | +| gb200 | `causal_conv1d_update` | shared | sglang, trtllm, vllm | sglang:462, trtllm:88, vllm:922 | 461 / 462 | 2 | 9.8 | 112.0 | 134.2 | +| gb200 | `chunk_gated_delta_rule` | shared | sglang, trtllm | sglang:4529, trtllm:854 | 824 / 4559 | 0 | 22.8 | 54.0 | 68.0 | +| gb200 | `chunk_gated_delta_rule_flashinfer` | shared | vllm | vllm:9408 | 0 / 4704 | 0 | — | — | — | +| gb200 | `flashinfer_gated_delta_rule_decode` | shared | sglang | sglang:52 | 0 / 52 | 0 | — | — | — | | gb200 | `fused_recurrent_gated_delta_rule` | shared | trtllm | trtllm:86 | 0 / 86 | 0 | — | — | — | -| gb200 | `fused_recurrent_gated_delta_rule_packed_decode` | shared | sglang, vllm | sglang:405, vllm:405 | 405 / 405 | 0 | 1.1 | 4.5 | 6.6 | -| gb300 | `causal_conv1d_fn` | shared | sglang, trtllm, vllm | sglang:4004, trtllm:837, vllm:4144 | 4042 / 4144 | 0 | 6.0 | 145.4 | 275.9 | -| gb300 | `causal_conv1d_update` | shared | sglang, trtllm, vllm | sglang:407, trtllm:88, vllm:407 | 407 / 407 | 0 | 9.7 | 147.1 | 182.3 | -| gb300 | `chunk_gated_delta_rule` | shared | sglang, trtllm | sglang:4004, trtllm:836 | 799 / 4041 | 0 | 20.3 | 59.3 | 67.4 | -| gb300 | `chunk_gated_delta_rule_flashinfer` | shared | vllm | vllm:4144 | 0 / 4144 | 0 | — | — | — | +| gb200 | `fused_recurrent_gated_delta_rule_packed_decode` | shared | sglang, vllm | sglang:457, vllm:914 | 457 / 457 | 0 | 1.0 | 4.4 | 6.1 | +| gb300 | `causal_conv1d_fn` | shared | sglang, trtllm, vllm | sglang:4529, trtllm:837, vllm:9408 | 4567 / 4704 | 7 | 5.6 | 140.3 | 276.0 | +| gb300 | `causal_conv1d_update` | shared | sglang, trtllm, vllm | sglang:462, trtllm:88, vllm:922 | 461 / 462 | 1 | 10.2 | 143.1 | 182.1 | +| gb300 | `chunk_gated_delta_rule` | shared | sglang, trtllm | sglang:4529, trtllm:836 | 799 / 4566 | 0 | 20.3 | 59.3 | 67.4 | +| gb300 | `chunk_gated_delta_rule_flashinfer` | shared | vllm | vllm:9408 | 0 / 4704 | 1 | — | — | — | +| gb300 | `flashinfer_gated_delta_rule_decode` | shared | sglang | sglang:52 | 0 / 52 | 0 | — | — | — | | gb300 | `fused_recurrent_gated_delta_rule` | shared | trtllm | trtllm:86 | 0 / 86 | 0 | — | — | — | -| gb300 | `fused_recurrent_gated_delta_rule_packed_decode` | shared | sglang, vllm | sglang:405, vllm:405 | 405 / 405 | 0 | 1.6 | 8.5 | 21.2 | -| h100_sxm | `causal_conv1d_fn` | shared | sglang, trtllm, vllm | sglang:4004, trtllm:816, vllm:4141 | 4004 / 4141 | 0 | 4.2 | 135.9 | 279.2 | -| h100_sxm | `causal_conv1d_update` | shared | sglang, trtllm, vllm | sglang:407, trtllm:88, vllm:407 | 407 / 407 | 0 | 14.6 | 101.5 | 136.6 | +| gb300 | `fused_recurrent_gated_delta_rule_packed_decode` | shared | sglang, vllm | sglang:457, vllm:914 | 457 / 457 | 1 | 1.4 | 7.2 | 18.1 | +| h100_sxm | `causal_conv1d_fn` | shared | sglang, trtllm, vllm | sglang:4004, trtllm:816, vllm:8824 | 4004 / 4683 | 0 | 9.0 | 134.5 | 278.5 | +| h100_sxm | `causal_conv1d_update` | shared | sglang, trtllm, vllm | sglang:407, trtllm:88, vllm:868 | 407 / 461 | 0 | 17.1 | 100.9 | 135.4 | | h100_sxm | `chunk_gated_delta_rule` | shared | sglang, trtllm | sglang:4004, trtllm:806 | 806 / 4004 | 0 | 42.2 | 59.4 | 69.5 | -| h100_sxm | `chunk_gated_delta_rule_flashinfer` | shared | vllm | vllm:4141 | 0 / 4141 | 0 | — | — | — | +| h100_sxm | `chunk_gated_delta_rule_flashinfer` | shared | vllm | vllm:8824 | 0 / 4683 | 0 | — | — | — | | h100_sxm | `fused_recurrent_gated_delta_rule` | shared | trtllm | trtllm:86 | 0 / 86 | 0 | — | — | — | -| h100_sxm | `fused_recurrent_gated_delta_rule_packed_decode` | shared | sglang, vllm | sglang:405, vllm:405 | 405 / 405 | 0 | 1.3 | 4.6 | 7.0 | -| h200_sxm | `causal_conv1d_fn` | shared | sglang, trtllm, vllm | sglang:4004, trtllm:798, vllm:4144 | 4017 / 4144 | 0 | 5.1 | 194.5 | 279.1 | -| h200_sxm | `causal_conv1d_update` | shared | sglang, trtllm, vllm | sglang:407, trtllm:88, vllm:407 | 407 / 407 | 0 | 14.6 | 99.3 | 133.2 | +| h100_sxm | `fused_recurrent_gated_delta_rule_packed_decode` | shared | sglang, vllm | sglang:405, vllm:862 | 405 / 457 | 0 | 4.3 | 11.0 | 13.4 | +| h200_sxm | `causal_conv1d_fn` | shared | sglang, trtllm, vllm | sglang:4004, trtllm:798, vllm:8847 | 4017 / 4703 | 8 | 5.2 | 194.5 | 279.0 | +| h200_sxm | `causal_conv1d_update` | shared | sglang, trtllm, vllm | sglang:407, trtllm:88, vllm:868 | 407 / 461 | 2 | 14.7 | 100.8 | 133.3 | | h200_sxm | `chunk_gated_delta_rule` | shared | sglang, trtllm | sglang:4004, trtllm:797 | 785 / 4016 | 0 | 44.5 | 130.0 | 145.6 | -| h200_sxm | `chunk_gated_delta_rule_flashinfer` | shared | vllm | vllm:4144 | 0 / 4144 | 0 | — | — | — | +| h200_sxm | `chunk_gated_delta_rule_flashinfer` | shared | vllm | vllm:8847 | 0 / 4703 | 1 | — | — | — | | h200_sxm | `fused_recurrent_gated_delta_rule` | shared | trtllm | trtllm:86 | 0 / 86 | 0 | — | — | — | -| h200_sxm | `fused_recurrent_gated_delta_rule_packed_decode` | shared | sglang, vllm | sglang:405, vllm:405 | 405 / 405 | 0 | 2.1 | 71.0 | 104.6 | +| h200_sxm | `fused_recurrent_gated_delta_rule_packed_decode` | shared | sglang, vllm | sglang:405, vllm:862 | 405 / 457 | 2 | 1.9 | 71.1 | 104.6 | | l40s | `causal_conv1d_fn` | shared | sglang, trtllm, vllm | sglang:4004, trtllm:786, vllm:3950 | 3956 / 4100 | 0 | 2.4 | 147.1 | 276.1 | | l40s | `causal_conv1d_update` | shared | sglang, trtllm, vllm | sglang:407, trtllm:88, vllm:407 | 407 / 407 | 0 | 14.1 | 137.2 | 168.2 | | l40s | `chunk_gated_delta_rule` | shared | sglang, trtllm | sglang:4004, trtllm:786 | 786 / 4004 | 0 | 21.5 | 104.5 | 118.2 | @@ -508,69 +541,77 @@ Rows with a blank/`` kernel_source are skipped during the scan (the cur | a100_sxm | `trt_flow_/smooth_quant_gemm_L96/PLUGIN_V2_SmoothQuantGemm_0` | shared | trtllm | trtllm:6048 | 0 / 6048 | 0 | — | — | — | | a100_sxm | `trt_flow_/weight_only_quant_matmul_L257/PLUGIN_V2_WeightOnlyQuantMatmul_0` | shared | trtllm | trtllm:12096 | 0 / 12096 | 0 | — | — | — | | a100_sxm | `vllm_default` | shared | vllm | vllm:9240 | 0 / 9240 | 0 | — | — | — | -| b200_sxm | `CutlassFP8ScaledMMLinearKernel` | shared | vllm | vllm:35742 | 0 / 35742 | 0 | — | — | — | -| b200_sxm | `CutlassFp8BlockScaledMMKernel` | shared | vllm | vllm:4736 | 0 / 4736 | 0 | — | — | — | -| b200_sxm | `DeepGemmFp8BlockScaledMMKernel` | shared | vllm | vllm:31006 | 0 / 31006 | 0 | — | — | — | -| b200_sxm | `FlashInferCuteDslNvFp4LinearKernel` | shared | vllm | vllm:35742 | 0 / 35742 | 0 | — | — | — | +| b200_sxm | `CutlassFP8ScaledMMLinearKernel` | shared | vllm | vllm:109150 | 0 / 37518 | 81 | — | — | — | +| b200_sxm | `CutlassFp8BlockScaledMMKernel` | shared | vllm | vllm:10138 | 0 / 5328 | 0 | — | — | — | +| b200_sxm | `DeepGemmFp8BlockScaledMMKernel` | shared | vllm | vllm:62086 | 0 / 31080 | 0 | — | — | — | +| b200_sxm | `FlashInferCuteDslNvFp4LinearKernel` | shared | vllm | vllm:109150 | 0 / 37518 | 482 | — | — | — | | b200_sxm | `deepgemm` | shared | trtllm | trtllm:29268 | 0 / 29268 | 0 | — | — | — | -| b200_sxm | `sglang_deepgemm_gemm_nt_f8f8bf16` | shared | sglang | sglang:29526 | 0 / 29526 | 0 | — | — | — | -| b200_sxm | `sglang_flashinfer_cutedsl_nvfp4` | shared | sglang | sglang:29526 | 0 / 29526 | 0 | — | — | — | -| b200_sxm | `sglang_sgl_kernel_fp8_scaled_mm` | shared | sglang | sglang:35742 | 0 / 35742 | 0 | — | — | — | -| b200_sxm | `sglang_torch_linear` | shared | sglang | sglang:35742 | 0 / 35742 | 0 | — | — | — | -| b200_sxm | `torch.nn.functional.linear` | shared | vllm | vllm:35742 | 0 / 35742 | 0 | — | — | — | +| b200_sxm | `sglang.srt.layers.vocab_parallel_embedding.ParallelLMHead.quant_method.apply/sglang.srt.layers.quantization.unquant.UnquantizedEmbeddingMethod` | shared | sglang | sglang:30 | 0 / 30 | 0 | — | — | — | +| b200_sxm | `sglang.srt.models.deepseek_v2.MoEGate.forward` | shared | sglang | sglang:15 | 0 / 15 | 0 | — | — | — | +| b200_sxm | `sglang_deepgemm_gemm_nt_f8f8bf16` | shared | sglang | sglang:59052 | 0 / 29526 | 15 | — | — | — | +| b200_sxm | `sglang_flashinfer_cutedsl_nvfp4` | shared | sglang | sglang:59052 | 0 / 29526 | 26 | — | — | — | +| b200_sxm | `sglang_sgl_kernel_fp8_scaled_mm` | shared | sglang | sglang:71558 | 0 / 35816 | 85 | — | — | — | +| b200_sxm | `sglang_torch_linear` | shared | sglang | sglang:71632 | 0 / 35890 | 86 | — | — | — | +| b200_sxm | `torch.nn.functional.linear` | shared | vllm | vllm:109150 | 0 / 37518 | 214 | — | — | — | | b200_sxm | `torch_flow` | shared | trtllm | trtllm:100888 | 0 / 100888 | 0 | — | — | — | -| b300_sxm | `CutlassFP8ScaledMMLinearKernel` | shared | vllm | vllm:35742 | 0 / 35742 | 0 | — | — | — | -| b300_sxm | `CutlassFp8BlockScaledMMKernel` | shared | vllm | vllm:4736 | 0 / 4736 | 0 | — | — | — | -| b300_sxm | `DeepGemmFp8BlockScaledMMKernel` | shared | vllm | vllm:31006 | 0 / 31006 | 0 | — | — | — | -| b300_sxm | `FlashInferCuteDslNvFp4LinearKernel` | shared | vllm | vllm:35742 | 0 / 35742 | 0 | — | — | — | +| b300_sxm | `CutlassFP8ScaledMMLinearKernel` | shared | vllm | vllm:109150 | 0 / 37518 | 52 | — | — | — | +| b300_sxm | `CutlassFp8BlockScaledMMKernel` | shared | vllm | vllm:10138 | 0 / 5328 | 0 | — | — | — | +| b300_sxm | `DeepGemmFp8BlockScaledMMKernel` | shared | vllm | vllm:62086 | 0 / 31080 | 0 | — | — | — | +| b300_sxm | `FlashInferCuteDslNvFp4LinearKernel` | shared | vllm | vllm:109150 | 0 / 37518 | 387 | — | — | — | | b300_sxm | `deepgemm` | shared | trtllm | trtllm:29268 | 0 / 29268 | 0 | — | — | — | -| b300_sxm | `sglang_deepgemm_gemm_nt_f8f8bf16` | shared | sglang | sglang:29526 | 0 / 29526 | 0 | — | — | — | -| b300_sxm | `sglang_flashinfer_cutedsl_nvfp4` | shared | sglang | sglang:29526 | 0 / 29526 | 0 | — | — | — | -| b300_sxm | `sglang_sgl_kernel_fp8_scaled_mm` | shared | sglang | sglang:35890 | 0 / 35890 | 0 | — | — | — | -| b300_sxm | `sglang_torch_linear` | shared | sglang | sglang:35964 | 0 / 35964 | 0 | — | — | — | -| b300_sxm | `torch.nn.functional.linear` | shared | vllm | vllm:35742 | 0 / 35742 | 0 | — | — | — | +| b300_sxm | `sglang_deepgemm_gemm_nt_f8f8bf16` | shared | sglang | sglang:59052 | 0 / 29526 | 10 | — | — | — | +| b300_sxm | `sglang_flashinfer_cutedsl_nvfp4` | shared | sglang | sglang:59052 | 0 / 29526 | 30 | — | — | — | +| b300_sxm | `sglang_sgl_kernel_fp8_scaled_mm` | shared | sglang | sglang:71706 | 0 / 35964 | 49 | — | — | — | +| b300_sxm | `sglang_torch_linear` | shared | sglang | sglang:71854 | 0 / 36112 | 104 | — | — | — | +| b300_sxm | `torch.nn.functional.linear` | shared | vllm | vllm:109150 | 0 / 37518 | 306 | — | — | — | | b300_sxm | `torch_flow` | shared | trtllm | trtllm:100888 | 0 / 100888 | 0 | — | — | — | | b60 | `vllm_default` | shared | vllm | vllm:40530 | 0 / 22176 | 1 | — | — | — | -| gb200 | `CutlassFP8ScaledMMLinearKernel` | shared | vllm | vllm:35742 | 0 / 35742 | 0 | — | — | — | -| gb200 | `CutlassFp8BlockScaledMMKernel` | shared | vllm | vllm:4736 | 0 / 4736 | 0 | — | — | — | -| gb200 | `DeepGemmFp8BlockScaledMMKernel` | shared | vllm | vllm:31006 | 0 / 31006 | 0 | — | — | — | -| gb200 | `FlashInferCuteDslNvFp4LinearKernel` | shared | vllm | vllm:35742 | 0 / 35742 | 0 | — | — | — | +| gb200 | `CutlassFP8ScaledMMLinearKernel` | shared | vllm | vllm:109150 | 0 / 37518 | 67 | — | — | — | +| gb200 | `CutlassFp8BlockScaledMMKernel` | shared | vllm | vllm:10138 | 0 / 5328 | 0 | — | — | — | +| gb200 | `DeepGemmFp8BlockScaledMMKernel` | shared | vllm | vllm:62086 | 0 / 31080 | 0 | — | — | — | +| gb200 | `FlashInferCuteDslNvFp4LinearKernel` | shared | vllm | vllm:109150 | 0 / 37518 | 281 | — | — | — | | gb200 | `deepgemm` | shared | trtllm | trtllm:29268 | 0 / 29268 | 0 | — | — | — | -| gb200 | `sglang_deepgemm_gemm_nt_f8f8bf16` | shared | sglang | sglang:29526 | 0 / 29526 | 0 | — | — | — | -| gb200 | `sglang_flashinfer_cutedsl_nvfp4` | shared | sglang | sglang:29526 | 0 / 29526 | 0 | — | — | — | -| gb200 | `sglang_sgl_kernel_fp8_scaled_mm` | shared | sglang | sglang:35742 | 0 / 35742 | 0 | — | — | — | -| gb200 | `sglang_torch_linear` | shared | sglang | sglang:35742 | 0 / 35742 | 0 | — | — | — | -| gb200 | `torch.nn.functional.linear` | shared | vllm | vllm:35742 | 0 / 35742 | 0 | — | — | — | +| gb200 | `sglang.srt.layers.vocab_parallel_embedding.ParallelLMHead.quant_method.apply/sglang.srt.layers.quantization.unquant.UnquantizedEmbeddingMethod` | shared | sglang | sglang:30 | 0 / 30 | 0 | — | — | — | +| gb200 | `sglang.srt.models.deepseek_v2.MoEGate.forward` | shared | sglang | sglang:15 | 0 / 15 | 0 | — | — | — | +| gb200 | `sglang_deepgemm_gemm_nt_f8f8bf16` | shared | sglang | sglang:59052 | 0 / 29526 | 14 | — | — | — | +| gb200 | `sglang_flashinfer_cutedsl_nvfp4` | shared | sglang | sglang:59052 | 0 / 29526 | 21 | — | — | — | +| gb200 | `sglang_sgl_kernel_fp8_scaled_mm` | shared | sglang | sglang:71558 | 0 / 35816 | 43 | — | — | — | +| gb200 | `sglang_torch_linear` | shared | sglang | sglang:71632 | 0 / 35890 | 61 | — | — | — | +| gb200 | `torch.nn.functional.linear` | shared | vllm | vllm:109150 | 0 / 37518 | 266 | — | — | — | | gb200 | `torch_flow` | shared | trtllm | trtllm:100888 | 0 / 100888 | 0 | — | — | — | -| gb300 | `CutlassFP8ScaledMMLinearKernel` | shared | vllm | vllm:35742 | 0 / 35742 | 0 | — | — | — | -| gb300 | `CutlassFp8BlockScaledMMKernel` | shared | vllm | vllm:4736 | 0 / 4736 | 0 | — | — | — | -| gb300 | `DeepGemmFp8BlockScaledMMKernel` | shared | vllm | vllm:31006 | 0 / 31006 | 0 | — | — | — | -| gb300 | `FlashInferCuteDslNvFp4LinearKernel` | shared | vllm | vllm:35742 | 0 / 35742 | 0 | — | — | — | +| gb300 | `CutlassFP8ScaledMMLinearKernel` | shared | vllm | vllm:109150 | 0 / 37518 | 98 | — | — | — | +| gb300 | `CutlassFp8BlockScaledMMKernel` | shared | vllm | vllm:10138 | 0 / 5328 | 0 | — | — | — | +| gb300 | `DeepGemmFp8BlockScaledMMKernel` | shared | vllm | vllm:62086 | 0 / 31080 | 0 | — | — | — | +| gb300 | `FlashInferCuteDslNvFp4LinearKernel` | shared | vllm | vllm:109150 | 0 / 37518 | 247 | — | — | — | | gb300 | `deepgemm` | shared | trtllm | trtllm:29268 | 0 / 29268 | 0 | — | — | — | -| gb300 | `sglang_deepgemm_gemm_nt_f8f8bf16` | shared | sglang | sglang:29526 | 0 / 29526 | 0 | — | — | — | -| gb300 | `sglang_flashinfer_cutedsl_nvfp4` | shared | sglang | sglang:29526 | 0 / 29526 | 0 | — | — | — | -| gb300 | `sglang_sgl_kernel_fp8_scaled_mm` | shared | sglang | sglang:35742 | 0 / 35742 | 0 | — | — | — | -| gb300 | `sglang_torch_linear` | shared | sglang | sglang:35742 | 0 / 35742 | 0 | — | — | — | -| gb300 | `torch.nn.functional.linear` | shared | vllm | vllm:35742 | 0 / 35742 | 0 | — | — | — | +| gb300 | `sglang_deepgemm_gemm_nt_f8f8bf16` | shared | sglang | sglang:59052 | 0 / 29526 | 4 | — | — | — | +| gb300 | `sglang_flashinfer_cutedsl_nvfp4` | shared | sglang | sglang:59052 | 0 / 29526 | 46 | — | — | — | +| gb300 | `sglang_sgl_kernel_fp8_scaled_mm` | shared | sglang | sglang:71558 | 0 / 35816 | 69 | — | — | — | +| gb300 | `sglang_torch_linear` | shared | sglang | sglang:71632 | 0 / 35890 | 85 | — | — | — | +| gb300 | `torch.nn.functional.linear` | shared | vllm | vllm:109150 | 0 / 37518 | 228 | — | — | — | | gb300 | `torch_flow` | shared | trtllm | trtllm:100888 | 0 / 100888 | 0 | — | — | — | -| h100_sxm | `CutlassFP8ScaledMMLinearKernel` | shared | vllm | vllm:35742 | 0 / 35742 | 0 | — | — | — | -| h100_sxm | `CutlassFp8BlockScaledMMKernel` | shared | vllm | vllm:4736 | 0 / 4736 | 0 | — | — | — | -| h100_sxm | `DeepGemmFp8BlockScaledMMKernel` | shared | vllm | vllm:23850 | 0 / 23850 | 0 | — | — | — | -| h100_sxm | `FlashInferFp8BlockScaledMMKernel` | shared | vllm | vllm:7118 | 0 / 7118 | 0 | — | — | — | +| h100_sxm | `CutlassFP8ScaledMMLinearKernel` | shared | vllm | vllm:73260 | 0 / 37518 | 1 | — | — | — | +| h100_sxm | `CutlassFp8BlockScaledMMKernel` | shared | vllm | vllm:5328 | 0 / 5328 | 0 | — | — | — | +| h100_sxm | `DeepGemmFp8BlockScaledMMKernel` | shared | vllm | vllm:23940 | 0 / 23940 | 0 | — | — | — | +| h100_sxm | `FlashInferFp8BlockScaledMMKernel` | shared | vllm | vllm:7140 | 0 / 7140 | 0 | — | — | — | | h100_sxm | `sglang` | shared | sglang | sglang:35280 | 0 / 35280 | 0 | — | — | — | +| h100_sxm | `sglang.srt.layers.vocab_parallel_embedding.ParallelLMHead.quant_method.apply/sglang.srt.layers.quantization.unquant.UnquantizedEmbeddingMethod` | shared | sglang | sglang:30 | 0 / 30 | 0 | — | — | — | +| h100_sxm | `sglang.srt.models.deepseek_v2.MoEGate.forward` | shared | sglang | sglang:15 | 0 / 15 | 0 | — | — | — | | h100_sxm | `sglang_deepgemm_gemm_nt_f8f8bf16` | shared | sglang | sglang:29526 | 0 / 29526 | 0 | — | — | — | | h100_sxm | `sglang_sgl_kernel_fp8_scaled_mm` | shared | sglang | sglang:35742 | 0 / 35742 | 0 | — | — | — | | h100_sxm | `sglang_torch_linear` | shared | sglang | sglang:35742 | 0 / 35742 | 0 | — | — | — | -| h100_sxm | `torch.nn.functional.linear` | shared | vllm | vllm:35742 | 0 / 35742 | 0 | — | — | — | +| h100_sxm | `torch.nn.functional.linear` | shared | vllm | vllm:73260 | 0 / 37518 | 23 | — | — | — | | h100_sxm | `torch_flow` | shared | trtllm | trtllm:100668 | 0 / 100668 | 0 | — | — | — | -| h200_sxm | `CutlassFP8ScaledMMLinearKernel` | shared | vllm | vllm:35742 | 0 / 35742 | 0 | — | — | — | -| h200_sxm | `CutlassFp8BlockScaledMMKernel` | shared | vllm | vllm:4736 | 0 / 4736 | 0 | — | — | — | -| h200_sxm | `DeepGemmFp8BlockScaledMMKernel` | shared | vllm | vllm:23882 | 0 / 23882 | 0 | — | — | — | -| h200_sxm | `FlashInferFp8BlockScaledMMKernel` | shared | vllm | vllm:7123 | 0 / 7123 | 0 | — | — | — | +| h200_sxm | `CutlassFP8ScaledMMLinearKernel` | shared | vllm | vllm:73260 | 0 / 37518 | 48 | — | — | — | +| h200_sxm | `CutlassFp8BlockScaledMMKernel` | shared | vllm | vllm:5328 | 0 / 5328 | 0 | — | — | — | +| h200_sxm | `DeepGemmFp8BlockScaledMMKernel` | shared | vllm | vllm:23940 | 0 / 23940 | 0 | — | — | — | +| h200_sxm | `FlashInferFp8BlockScaledMMKernel` | shared | vllm | vllm:7140 | 0 / 7140 | 0 | — | — | — | +| h200_sxm | `sglang.srt.layers.vocab_parallel_embedding.ParallelLMHead.quant_method.apply/sglang.srt.layers.quantization.unquant.UnquantizedEmbeddingMethod` | shared | sglang | sglang:30 | 0 / 30 | 0 | — | — | — | +| h200_sxm | `sglang.srt.models.deepseek_v2.MoEGate.forward` | shared | sglang | sglang:15 | 0 / 15 | 0 | — | — | — | | h200_sxm | `sglang_deepgemm_gemm_nt_f8f8bf16` | shared | sglang | sglang:29526 | 0 / 29526 | 0 | — | — | — | | h200_sxm | `sglang_sgl_kernel_fp8_scaled_mm` | shared | sglang | sglang:35742 | 0 / 35742 | 0 | — | — | — | | h200_sxm | `sglang_torch_linear` | shared | sglang | sglang:35742 | 0 / 35742 | 0 | — | — | — | -| h200_sxm | `torch.nn.functional.linear` | shared | vllm | vllm:35742 | 0 / 35742 | 0 | — | — | — | +| h200_sxm | `torch.nn.functional.linear` | shared | vllm | vllm:73260 | 0 / 37518 | 67 | — | — | — | | h200_sxm | `torch_flow` | shared | trtllm | trtllm:100668 | 0 / 100668 | 0 | — | — | — | | l40s | `CutlassFP8ScaledMMLinearKernel` | shared | vllm | vllm:35718 | 0 / 35718 | 0 | — | — | — | | l40s | `TritonFp8BlockScaledMMKernel` | shared | vllm | vllm:35676 | 0 / 35676 | 0 | — | — | — | @@ -595,45 +636,49 @@ Rows with a blank/`` kernel_source are skipped during the scan (the cur | a100_sxm | `torch_flow` | shared | trtllm | trtllm:5026 | 0 / 5026 | 0 | — | — | — | | a100_sxm | `vllm_flash_attn` | shared | vllm | vllm:5431 | 0 / 5431 | 0 | — | — | — | | b200_sxm | `flashinfer` | shared | sglang | sglang:3570 | 0 / 3570 | 0 | — | — | — | -| b200_sxm | `torch_flow` | shared | trtllm | trtllm:46980 | 0 / 46980 | 0 | — | — | — | +| b200_sxm | `torch_flow` | shared | trtllm | trtllm:48314 | 0 / 48314 | 0 | — | — | — | | b200_sxm | `torch_flow_flashinfer` | shared | trtllm | trtllm:3540 | 0 / 3540 | 0 | — | — | — | | b200_sxm | `triton` | shared | sglang | sglang:8620 | 0 / 8620 | 0 | — | — | — | -| b200_sxm | `trtllm_mha` | shared | sglang | sglang:36311 | 0 / 36311 | 0 | — | — | — | +| b200_sxm | `trtllm_mha` | shared | sglang | sglang:37643 | 0 / 37643 | 0 | — | — | — | | b200_sxm | `vllm_flashinfer` | shared | vllm | vllm:36240 | 0 / 36240 | 0 | — | — | — | -| b200_sxm | `vllm_flashinfer_trtllmdecode` | shared | vllm | vllm:59582 | 0 / 59582 | 0 | — | — | — | -| b200_sxm | `vllm_triton_attn` | shared | vllm | vllm:4456 | 0 / 4456 | 0 | — | — | — | +| b200_sxm | `vllm_flashinfer_flashinfertrtllmapidecode` | shared | vllm | vllm:63180 | 0 / 63180 | 0 | — | — | — | +| b200_sxm | `vllm_flashinfer_trtllmdecode` | shared | vllm | vllm:60916 | 0 / 60916 | 0 | — | — | — | +| b200_sxm | `vllm_triton_attn` | shared | vllm | vllm:6684 | 0 / 4456 | 17 | — | — | — | | b300_sxm | `flashinfer` | shared | sglang | sglang:3570 | 0 / 3570 | 0 | — | — | — | | b300_sxm | `torch_flow` | shared | trtllm | trtllm:46980 | 0 / 46980 | 0 | — | — | — | | b300_sxm | `torch_flow_flashinfer` | shared | trtllm | trtllm:3540 | 0 / 3540 | 0 | — | — | — | | b300_sxm | `triton` | shared | sglang | sglang:8620 | 0 / 8620 | 0 | — | — | — | | b300_sxm | `trtllm_mha` | shared | sglang | sglang:37490 | 0 / 37490 | 0 | — | — | — | +| b300_sxm | `vllm_flashinfer_flashinfertrtllmapidecode` | shared | vllm | vllm:63180 | 0 / 63180 | 0 | — | — | — | | b300_sxm | `vllm_flashinfer_trtllmdecode` | shared | vllm | vllm:58510 | 0 / 58510 | 0 | — | — | — | -| b300_sxm | `vllm_triton_attn` | shared | vllm | vllm:2228 | 0 / 2228 | 0 | — | — | — | +| b300_sxm | `vllm_triton_attn` | shared | vllm | vllm:4456 | 0 / 2228 | 7 | — | — | — | | b60 | `vllm_flash_attn` | shared | vllm | vllm:57209 | 0 / 30948 | 3 | — | — | — | | gb200 | `flashinfer` | shared | sglang | sglang:3570 | 0 / 3570 | 0 | — | — | — | -| gb200 | `torch_flow` | shared | trtllm | trtllm:46982 | 0 / 46982 | 0 | — | — | — | +| gb200 | `torch_flow` | shared | trtllm | trtllm:48316 | 0 / 48316 | 0 | — | — | — | | gb200 | `torch_flow_flashinfer` | shared | trtllm | trtllm:3540 | 0 / 3540 | 0 | — | — | — | | gb200 | `triton` | shared | sglang | sglang:8620 | 0 / 8620 | 0 | — | — | — | -| gb200 | `trtllm_mha` | shared | sglang | sglang:34748 | 0 / 34748 | 0 | — | — | — | -| gb200 | `vllm_flashinfer_trtllmdecode` | shared | vllm | vllm:58510 | 0 / 58510 | 0 | — | — | — | -| gb200 | `vllm_triton_attn` | shared | vllm | vllm:2228 | 0 / 2228 | 0 | — | — | — | +| gb200 | `trtllm_mha` | shared | sglang | sglang:36082 | 0 / 36082 | 0 | — | — | — | +| gb200 | `vllm_flashinfer_flashinfertrtllmapidecode` | shared | vllm | vllm:63180 | 0 / 63180 | 0 | — | — | — | +| gb200 | `vllm_flashinfer_trtllmdecode` | shared | vllm | vllm:59844 | 0 / 59844 | 0 | — | — | — | +| gb200 | `vllm_triton_attn` | shared | vllm | vllm:4456 | 0 / 2228 | 7 | — | — | — | | gb300 | `flashinfer` | shared | sglang | sglang:3570 | 0 / 3570 | 0 | — | — | — | -| gb300 | `torch_flow` | shared | trtllm | trtllm:46982 | 0 / 46982 | 0 | — | — | — | +| gb300 | `torch_flow` | shared | trtllm | trtllm:48316 | 0 / 48316 | 0 | — | — | — | | gb300 | `torch_flow_flashinfer` | shared | trtllm | trtllm:3540 | 0 / 3540 | 0 | — | — | — | | gb300 | `triton` | shared | sglang | sglang:8620 | 0 / 8620 | 0 | — | — | — | -| gb300 | `trtllm_mha` | shared | sglang | sglang:34745 | 0 / 34745 | 0 | — | — | — | -| gb300 | `vllm_flashinfer_trtllmdecode` | shared | vllm | vllm:58510 | 0 / 58510 | 0 | — | — | — | -| gb300 | `vllm_triton_attn` | shared | vllm | vllm:2228 | 0 / 2228 | 0 | — | — | — | +| gb300 | `trtllm_mha` | shared | sglang | sglang:36078 | 0 / 36078 | 0 | — | — | — | +| gb300 | `vllm_flashinfer_flashinfertrtllmapidecode` | shared | vllm | vllm:63180 | 0 / 63180 | 0 | — | — | — | +| gb300 | `vllm_flashinfer_trtllmdecode` | shared | vllm | vllm:59844 | 0 / 59844 | 0 | — | — | — | +| gb300 | `vllm_triton_attn` | shared | vllm | vllm:4456 | 0 / 2228 | 7 | — | — | — | | h100_sxm | `fa3` | shared | sglang | sglang:37176 | 0 / 37176 | 0 | — | — | — | | h100_sxm | `torch_flow` | shared | trtllm | trtllm:52033 | 0 / 52033 | 0 | — | — | — | | h100_sxm | `triton` | shared | sglang | sglang:3292 | 0 / 3292 | 0 | — | — | — | -| h100_sxm | `vllm_flash_attn_fa3` | shared | vllm | vllm:61366 | 0 / 61366 | 0 | — | — | — | -| h100_sxm | `vllm_flash_attn_fa4` | shared | vllm | vllm:800 | 0 / 800 | 0 | — | — | — | -| h200_sxm | `fa3` | shared | sglang | sglang:37176 | 0 / 37176 | 0 | — | — | — | -| h200_sxm | `torch_flow` | shared | trtllm | trtllm:52033 | 0 / 52033 | 0 | — | — | — | +| h100_sxm | `vllm_flash_attn_fa3` | shared | vllm | vllm:127146 | 0 / 65780 | 23 | — | — | — | +| h100_sxm | `vllm_flash_attn_fa4` | shared | vllm | vllm:1728 | 0 / 928 | 1 | — | — | — | +| h200_sxm | `fa3` | shared | sglang | sglang:38510 | 0 / 38510 | 0 | — | — | — | +| h200_sxm | `torch_flow` | shared | trtllm | trtllm:53367 | 0 / 53367 | 0 | — | — | — | | h200_sxm | `triton` | shared | sglang | sglang:3292 | 0 / 3292 | 0 | — | — | — | -| h200_sxm | `vllm_flash_attn_fa3` | shared | vllm | vllm:61366 | 0 / 61366 | 0 | — | — | — | -| h200_sxm | `vllm_flash_attn_fa4` | shared | vllm | vllm:800 | 0 / 800 | 0 | — | — | — | +| h200_sxm | `vllm_flash_attn_fa3` | shared | vllm | vllm:128480 | 0 / 65780 | 485 | — | — | — | +| h200_sxm | `vllm_flash_attn_fa4` | shared | vllm | vllm:1728 | 0 / 928 | 4 | — | — | — | | l40s | `flashinfer` | shared | sglang | sglang:29158 | 0 / 29158 | 0 | — | — | — | | l40s | `torch_flow` | shared | trtllm | trtllm:52714 | 0 / 52714 | 0 | — | — | — | | l40s | `triton` | shared | sglang | sglang:9384 | 0 / 9384 | 0 | — | — | — | @@ -808,48 +853,48 @@ Rows with a blank/`` kernel_source are skipped during the scan (the cur | b200_sxm | `trtllm_mhc_pre_dg_nosplit` | shared | trtllm | trtllm:27 | 0 / 27 | 0 | — | — | — | | b200_sxm | `trtllm_mhc_pre_dg_splitk` | shared | trtllm | trtllm:26 | 0 / 26 | 0 | — | — | — | | b200_sxm | `trtllm_mhc_pre_fma` | shared | trtllm | trtllm:16 | 0 / 16 | 0 | — | — | — | -| b200_sxm | `vllm.model_executor.kernels.mhc.tilelang.mhc_post_tilelang` | shared | vllm | vllm:70 | 0 / 70 | 0 | — | — | — | -| b200_sxm | `vllm.model_executor.kernels.mhc.tilelang.mhc_pre_tilelang` | shared | vllm | vllm:64 | 0 / 64 | 0 | — | — | — | +| b200_sxm | `vllm.model_executor.kernels.mhc.tilelang.mhc_post_tilelang` | shared | vllm | vllm:140 | 0 / 70 | 8 | — | — | — | +| b200_sxm | `vllm.model_executor.kernels.mhc.tilelang.mhc_pre_tilelang` | shared | vllm | vllm:134 | 0 / 70 | 2 | — | — | — | | b300_sxm | `sglang_tilelang_mhc_post` | shared | sglang | sglang:70 | 0 / 70 | 0 | — | — | — | | b300_sxm | `sglang_tilelang_mhc_pre` | shared | sglang | sglang:70 | 0 / 70 | 0 | — | — | — | | b300_sxm | `trtllm_mhc_post_mapping` | shared | trtllm | trtllm:70 | 0 / 70 | 0 | — | — | — | | b300_sxm | `trtllm_mhc_pre_dg_nosplit` | shared | trtllm | trtllm:28 | 0 / 28 | 0 | — | — | — | | b300_sxm | `trtllm_mhc_pre_dg_splitk` | shared | trtllm | trtllm:26 | 0 / 26 | 0 | — | — | — | | b300_sxm | `trtllm_mhc_pre_fma` | shared | trtllm | trtllm:16 | 0 / 16 | 0 | — | — | — | -| b300_sxm | `vllm.model_executor.kernels.mhc.tilelang.mhc_post_tilelang` | shared | vllm | vllm:70 | 0 / 70 | 0 | — | — | — | -| b300_sxm | `vllm.model_executor.kernels.mhc.tilelang.mhc_pre_tilelang` | shared | vllm | vllm:63 | 0 / 63 | 0 | — | — | — | +| b300_sxm | `vllm.model_executor.kernels.mhc.tilelang.mhc_post_tilelang` | shared | vllm | vllm:140 | 0 / 70 | 9 | — | — | — | +| b300_sxm | `vllm.model_executor.kernels.mhc.tilelang.mhc_pre_tilelang` | shared | vllm | vllm:133 | 0 / 70 | 5 | — | — | — | | gb200 | `sglang_tilelang_mhc_post` | shared | sglang | sglang:70 | 0 / 70 | 0 | — | — | — | | gb200 | `sglang_tilelang_mhc_pre` | shared | sglang | sglang:70 | 0 / 70 | 0 | — | — | — | | gb200 | `trtllm_mhc_post_mapping` | shared | trtllm | trtllm:70 | 0 / 70 | 0 | — | — | — | | gb200 | `trtllm_mhc_pre_dg_nosplit` | shared | trtllm | trtllm:26 | 0 / 26 | 0 | — | — | — | | gb200 | `trtllm_mhc_pre_dg_splitk` | shared | trtllm | trtllm:27 | 0 / 27 | 0 | — | — | — | | gb200 | `trtllm_mhc_pre_fma` | shared | trtllm | trtllm:16 | 0 / 16 | 0 | — | — | — | -| gb200 | `vllm.model_executor.kernels.mhc.tilelang.mhc_post_tilelang` | shared | vllm | vllm:70 | 0 / 70 | 0 | — | — | — | -| gb200 | `vllm.model_executor.kernels.mhc.tilelang.mhc_pre_tilelang` | shared | vllm | vllm:70 | 0 / 70 | 0 | — | — | — | +| gb200 | `vllm.model_executor.kernels.mhc.tilelang.mhc_post_tilelang` | shared | vllm | vllm:140 | 0 / 70 | 6 | — | — | — | +| gb200 | `vllm.model_executor.kernels.mhc.tilelang.mhc_pre_tilelang` | shared | vllm | vllm:140 | 0 / 70 | 7 | — | — | — | | gb300 | `sglang_tilelang_mhc_post` | shared | sglang | sglang:70 | 0 / 70 | 0 | — | — | — | | gb300 | `sglang_tilelang_mhc_pre` | shared | sglang | sglang:70 | 0 / 70 | 0 | — | — | — | | gb300 | `trtllm_mhc_post_mapping` | shared | trtllm | trtllm:70 | 0 / 70 | 0 | — | — | — | | gb300 | `trtllm_mhc_pre_dg_nosplit` | shared | trtllm | trtllm:28 | 0 / 28 | 0 | — | — | — | | gb300 | `trtllm_mhc_pre_dg_splitk` | shared | trtllm | trtllm:26 | 0 / 26 | 0 | — | — | — | | gb300 | `trtllm_mhc_pre_fma` | shared | trtllm | trtllm:16 | 0 / 16 | 0 | — | — | — | -| gb300 | `vllm.model_executor.kernels.mhc.tilelang.mhc_post_tilelang` | shared | vllm | vllm:70 | 0 / 70 | 0 | — | — | — | -| gb300 | `vllm.model_executor.kernels.mhc.tilelang.mhc_pre_tilelang` | shared | vllm | vllm:70 | 0 / 70 | 0 | — | — | — | +| gb300 | `vllm.model_executor.kernels.mhc.tilelang.mhc_post_tilelang` | shared | vllm | vllm:140 | 0 / 70 | 10 | — | — | — | +| gb300 | `vllm.model_executor.kernels.mhc.tilelang.mhc_pre_tilelang` | shared | vllm | vllm:140 | 0 / 70 | 13 | — | — | — | | h100_sxm | `sglang_tilelang_mhc_post` | shared | sglang | sglang:68 | 0 / 68 | 0 | — | — | — | | h100_sxm | `sglang_tilelang_mhc_pre` | shared | sglang | sglang:70 | 0 / 70 | 0 | — | — | — | | h100_sxm | `trtllm_mhc_post_mapping` | shared | trtllm | trtllm:69 | 0 / 69 | 0 | — | — | — | | h100_sxm | `trtllm_mhc_pre_dg_nosplit` | shared | trtllm | trtllm:24 | 0 / 24 | 0 | — | — | — | | h100_sxm | `trtllm_mhc_pre_dg_splitk` | shared | trtllm | trtllm:27 | 0 / 27 | 0 | — | — | — | | h100_sxm | `trtllm_mhc_pre_fma` | shared | trtllm | trtllm:16 | 0 / 16 | 0 | — | — | — | -| h100_sxm | `vllm.model_executor.kernels.mhc.tilelang.mhc_post_tilelang` | shared | vllm | vllm:69 | 0 / 69 | 0 | — | — | — | -| h100_sxm | `vllm.model_executor.kernels.mhc.tilelang.mhc_pre_tilelang` | shared | vllm | vllm:70 | 0 / 70 | 0 | — | — | — | +| h100_sxm | `vllm.model_executor.kernels.mhc.tilelang.mhc_post_tilelang` | shared | vllm | vllm:138 | 0 / 69 | 5 | — | — | — | +| h100_sxm | `vllm.model_executor.kernels.mhc.tilelang.mhc_pre_tilelang` | shared | vllm | vllm:140 | 0 / 70 | 0 | — | — | — | | h200_sxm | `sglang_tilelang_mhc_post` | shared | sglang | sglang:70 | 0 / 70 | 0 | — | — | — | | h200_sxm | `sglang_tilelang_mhc_pre` | shared | sglang | sglang:70 | 0 / 70 | 0 | — | — | — | | h200_sxm | `trtllm_mhc_post_mapping` | shared | trtllm | trtllm:70 | 0 / 70 | 0 | — | — | — | | h200_sxm | `trtllm_mhc_pre_dg_nosplit` | shared | trtllm | trtllm:26 | 0 / 26 | 0 | — | — | — | | h200_sxm | `trtllm_mhc_pre_dg_splitk` | shared | trtllm | trtllm:24 | 0 / 24 | 0 | — | — | — | | h200_sxm | `trtllm_mhc_pre_fma` | shared | trtllm | trtllm:19 | 0 / 19 | 0 | — | — | — | -| h200_sxm | `vllm.model_executor.kernels.mhc.tilelang.mhc_post_tilelang` | shared | vllm | vllm:70 | 0 / 70 | 0 | — | — | — | -| h200_sxm | `vllm.model_executor.kernels.mhc.tilelang.mhc_pre_tilelang` | shared | vllm | vllm:70 | 0 / 70 | 0 | — | — | — | +| h200_sxm | `vllm.model_executor.kernels.mhc.tilelang.mhc_post_tilelang` | shared | vllm | vllm:140 | 0 / 70 | 5 | — | — | — | +| h200_sxm | `vllm.model_executor.kernels.mhc.tilelang.mhc_pre_tilelang` | shared | vllm | vllm:140 | 0 / 70 | 8 | — | — | — | | l40s | `trtllm_mhc_post_mapping` | shared | trtllm | trtllm:67 | 0 / 67 | 0 | — | — | — | | l40s | `trtllm_mhc_pre_fma` | shared | trtllm | trtllm:65 | 0 / 65 | 0 | — | — | — | | l40s | `vllm.model_executor.kernels.mhc.tilelang.mhc_post_tilelang` | shared | vllm | vllm:67 | 0 / 67 | 0 | — | — | — | @@ -866,34 +911,34 @@ Rows with a blank/`` kernel_source are skipped during the scan (the cur | a100_sxm | `default` | shared_fallback | sglang, trtllm | sglang:424, trtllm:424 | 224 / 224 | 0 | 1.4 | 18.4 | 115.9 | | b200_sxm | `sglang_sgl_kernel_bmm_fp8` | shared | sglang | sglang:636 | 0 / 336 | 0 | — | — | — | | b200_sxm | `sglang_torch_bmm` | shared | sglang | sglang:636 | 0 / 336 | 0 | — | — | — | -| b200_sxm | `trtllm_bmm_out` | shared | trtllm | trtllm:424 | 0 / 224 | 0 | — | — | — | -| b200_sxm | `trtllm_bmm_out_dequant_bf16` | shared | trtllm | trtllm:424 | 0 / 224 | 0 | — | — | — | -| b200_sxm | `vllm_torch_bmm` | shared | vllm | vllm:636 | 0 / 336 | 0 | — | — | — | +| b200_sxm | `trtllm_bmm_out` | shared | trtllm | trtllm:424 | 0 / 424 | 0 | — | — | — | +| b200_sxm | `trtllm_bmm_out_dequant_bf16` | shared | trtllm | trtllm:424 | 0 / 424 | 0 | — | — | — | +| b200_sxm | `vllm_torch_bmm` | shared | vllm | vllm:1272 | 0 / 336 | 1 | — | — | — | | b300_sxm | `sglang_sgl_kernel_bmm_fp8` | shared | sglang | sglang:848 | 0 / 224 | 0 | — | — | — | | b300_sxm | `sglang_torch_bmm` | shared | sglang | sglang:848 | 0 / 224 | 0 | — | — | — | | b300_sxm | `trtllm_bmm_out` | shared | trtllm | trtllm:424 | 0 / 224 | 0 | — | — | — | | b300_sxm | `trtllm_bmm_out_dequant_bf16` | shared | trtllm | trtllm:424 | 0 / 224 | 0 | — | — | — | -| b300_sxm | `vllm_torch_bmm` | shared | vllm | vllm:636 | 0 / 336 | 0 | — | — | — | +| b300_sxm | `vllm_torch_bmm` | shared | vllm | vllm:1272 | 0 / 336 | 3 | — | — | — | | gb200 | `sglang_sgl_kernel_bmm_fp8` | shared | sglang | sglang:848 | 0 / 224 | 0 | — | — | — | | gb200 | `sglang_torch_bmm` | shared | sglang | sglang:848 | 0 / 224 | 0 | — | — | — | | gb200 | `trtllm_bmm_out` | shared | trtllm | trtllm:424 | 0 / 224 | 0 | — | — | — | | gb200 | `trtllm_bmm_out_dequant_bf16` | shared | trtllm | trtllm:424 | 0 / 224 | 0 | — | — | — | -| gb200 | `vllm_torch_bmm` | shared | vllm | vllm:636 | 0 / 336 | 0 | — | — | — | +| gb200 | `vllm_torch_bmm` | shared | vllm | vllm:1272 | 0 / 336 | 1 | — | — | — | | gb300 | `sglang_sgl_kernel_bmm_fp8` | shared | sglang | sglang:848 | 0 / 224 | 0 | — | — | — | | gb300 | `sglang_torch_bmm` | shared | sglang | sglang:848 | 0 / 224 | 0 | — | — | — | | gb300 | `trtllm_bmm_out` | shared | trtllm | trtllm:424 | 0 / 224 | 0 | — | — | — | | gb300 | `trtllm_bmm_out_dequant_bf16` | shared | trtllm | trtllm:424 | 0 / 224 | 0 | — | — | — | -| gb300 | `vllm_torch_bmm` | shared | vllm | vllm:1072 | 0 / 336 | 2 | — | — | — | +| gb300 | `vllm_torch_bmm` | shared | vllm | vllm:1708 | 0 / 336 | 4 | — | — | — | | h100_sxm | `sglang_sgl_kernel_bmm_fp8` | shared | sglang | sglang:848 | 0 / 224 | 0 | — | — | — | | h100_sxm | `sglang_torch_bmm` | shared | sglang | sglang:848 | 0 / 224 | 0 | — | — | — | | h100_sxm | `trtllm_bmm_out` | shared | trtllm | trtllm:424 | 0 / 224 | 0 | — | — | — | | h100_sxm | `trtllm_fp8_block_scaling_bmm_out` | shared | trtllm | trtllm:424 | 0 / 224 | 0 | — | — | — | -| h100_sxm | `vllm_torch_bmm` | shared | vllm | vllm:636 | 0 / 336 | 0 | — | — | — | +| h100_sxm | `vllm_torch_bmm` | shared | vllm | vllm:1272 | 0 / 336 | 1 | — | — | — | | h200_sxm | `sglang_sgl_kernel_bmm_fp8` | shared | sglang | sglang:424 | 0 / 224 | 0 | — | — | — | | h200_sxm | `sglang_torch_bmm` | shared | sglang | sglang:424 | 0 / 224 | 0 | — | — | — | | h200_sxm | `trtllm_bmm_out` | shared | trtllm | trtllm:424 | 0 / 224 | 0 | — | — | — | | h200_sxm | `trtllm_fp8_block_scaling_bmm_out` | shared | trtllm | trtllm:424 | 0 / 224 | 0 | — | — | — | -| h200_sxm | `vllm_torch_bmm` | shared | vllm | vllm:636 | 0 / 336 | 0 | — | — | — | +| h200_sxm | `vllm_torch_bmm` | shared | vllm | vllm:1272 | 0 / 336 | 2 | — | — | — | | l40s | `default` | shared_fallback | trtllm | trtllm:848 | 0 / 448 | 0 | — | — | — | | l40s | `sglang_sgl_kernel_bmm_fp8` | shared | sglang | sglang:424 | 0 / 224 | 0 | — | — | — | | l40s | `sglang_torch_bmm` | shared | sglang | sglang:424 | 0 / 224 | 0 | — | — | — | @@ -908,29 +953,31 @@ Rows with a blank/`` kernel_source are skipped during the scan (the cur | system | kernel_source | tier | frameworks | rows_per_fw | overlap_keys | dedup rows | median % | p95 % | max % | |---|---|---|---|---|---|---|---|---|---| -| b200_sxm | `FLASHINFER_MLA` | shared | vllm | vllm:648 | 0 / 648 | 0 | — | — | — | -| b200_sxm | `FLASH_ATTN` | shared | vllm | vllm:5424 | 0 / 5424 | 0 | — | — | — | -| b200_sxm | `TRTLLM_RAGGED` | shared | vllm | vllm:2712 | 0 / 2712 | 0 | — | — | — | +| b200_sxm | `FLASHINFER_MLA` | shared | vllm | vllm:1503 | 0 / 855 | 5 | — | — | — | +| b200_sxm | `FLASH_ATTN` | shared | vllm | vllm:13560 | 0 / 8136 | 58 | — | — | — | +| b200_sxm | `TRTLLM_RAGGED` | shared | vllm | vllm:6780 | 0 / 4068 | 27 | — | — | — | | b200_sxm | `default` | shared_fallback | trtllm | trtllm:5856 | 0 / 5856 | 0 | — | — | — | -| b300_sxm | `FLASHINFER_MLA` | shared | vllm | vllm:648 | 0 / 648 | 0 | — | — | — | -| b300_sxm | `FLASH_ATTN` | shared | vllm | vllm:5424 | 0 / 5424 | 0 | — | — | — | -| b300_sxm | `TRTLLM_RAGGED` | shared | vllm | vllm:2712 | 0 / 2712 | 0 | — | — | — | +| b200_sxm | `mla_trtllm_mla` | shared | sglang | sglang:218 | 0 / 218 | 0 | — | — | — | +| b300_sxm | `FLASHINFER_MLA` | shared | vllm | vllm:1503 | 0 / 855 | 0 | — | — | — | +| b300_sxm | `FLASH_ATTN` | shared | vllm | vllm:13560 | 0 / 8136 | 0 | — | — | — | +| b300_sxm | `TRTLLM_RAGGED` | shared | vllm | vllm:6780 | 0 / 4068 | 0 | — | — | — | | b300_sxm | `default` | shared_fallback | trtllm | trtllm:5856 | 0 / 5856 | 0 | — | — | — | -| gb200 | `FLASHINFER_MLA` | shared | vllm | vllm:648 | 0 / 648 | 0 | — | — | — | -| gb200 | `FLASH_ATTN` | shared | vllm | vllm:5424 | 0 / 5424 | 0 | — | — | — | -| gb200 | `TRTLLM_RAGGED` | shared | vllm | vllm:2712 | 0 / 2712 | 0 | — | — | — | +| b300_sxm | `mla_trtllm_mla` | shared | sglang | sglang:218 | 0 / 218 | 0 | — | — | — | +| gb200 | `FLASHINFER_MLA` | shared | vllm | vllm:1503 | 0 / 855 | 0 | — | — | — | +| gb200 | `FLASH_ATTN` | shared | vllm | vllm:13560 | 0 / 8136 | 0 | — | — | — | +| gb200 | `TRTLLM_RAGGED` | shared | vllm | vllm:6780 | 0 / 4068 | 0 | — | — | — | | gb200 | `default` | shared_fallback | trtllm | trtllm:5856 | 0 / 5856 | 0 | — | — | — | -| gb300 | `FLASHINFER_MLA` | shared | vllm | vllm:2264 | 0 / 1620 | 0 | — | — | — | -| gb300 | `FLASH_ATTN` | shared | vllm | vllm:18925 | 0 / 13560 | 0 | — | — | — | -| gb300 | `TRTLLM_RAGGED` | shared | vllm | vllm:9455 | 0 / 6780 | 0 | — | — | — | +| gb300 | `FLASHINFER_MLA` | shared | vllm | vllm:3119 | 0 / 1827 | 6 | — | — | — | +| gb300 | `FLASH_ATTN` | shared | vllm | vllm:27061 | 0 / 16272 | 49 | — | — | — | +| gb300 | `TRTLLM_RAGGED` | shared | vllm | vllm:13523 | 0 / 8136 | 15 | — | — | — | | gb300 | `default` | shared_fallback | trtllm | trtllm:5856 | 0 / 5856 | 0 | — | — | — | -| h100_sxm | `FLASHMLA` | shared | vllm | vllm:702 | 0 / 702 | 0 | — | — | — | -| h100_sxm | `FLASH_ATTN` | shared | vllm | vllm:2176 | 0 / 2176 | 0 | — | — | — | -| h100_sxm | `FLASH_ATTN_MLA` | shared | vllm | vllm:1008 | 0 / 1008 | 0 | — | — | — | +| h100_sxm | `FLASHMLA` | shared | vllm | vllm:1746 | 0 / 1044 | 0 | — | — | — | +| h100_sxm | `FLASH_ATTN` | shared | vllm | vllm:5440 | 0 / 3264 | 0 | — | — | — | +| h100_sxm | `FLASH_ATTN_MLA` | shared | vllm | vllm:2520 | 0 / 1512 | 0 | — | — | — | | h100_sxm | `default` | shared_fallback | trtllm | trtllm:3866 | 0 / 3866 | 0 | — | — | — | -| h200_sxm | `FLASHMLA` | shared | vllm | vllm:702 | 0 / 702 | 0 | — | — | — | -| h200_sxm | `FLASH_ATTN` | shared | vllm | vllm:2176 | 0 / 2176 | 0 | — | — | — | -| h200_sxm | `FLASH_ATTN_MLA` | shared | vllm | vllm:1008 | 0 / 1008 | 0 | — | — | — | +| h200_sxm | `FLASHMLA` | shared | vllm | vllm:1746 | 0 / 1044 | 0 | — | — | — | +| h200_sxm | `FLASH_ATTN` | shared | vllm | vllm:5440 | 0 / 3264 | 0 | — | — | — | +| h200_sxm | `FLASH_ATTN_MLA` | shared | vllm | vllm:2520 | 0 / 1512 | 0 | — | — | — | | h200_sxm | `default` | shared_fallback | trtllm | trtllm:3869 | 0 / 3869 | 0 | — | — | — | | l40s | `FLASH_ATTN` | shared | vllm | vllm:1790 | 0 / 1790 | 0 | — | — | — | | l40s | `TRITON_MLA` | shared | vllm | vllm:144 | 0 / 144 | 0 | — | — | — | @@ -942,19 +989,19 @@ Rows with a blank/`` kernel_source are skipped during the scan (the cur | system | kernel_source | tier | frameworks | rows_per_fw | overlap_keys | dedup rows | median % | p95 % | max % | |---|---|---|---|---|---|---|---|---|---| -| b200_sxm | `FLASHINFER_MLA` | shared | vllm | vllm:8832 | 0 / 8832 | 0 | — | — | — | +| b200_sxm | `FLASHINFER_MLA` | shared | vllm | vllm:20232 | 0 / 11400 | 70 | — | — | — | | b200_sxm | `default` | shared_fallback | trtllm | trtllm:8831 | 0 / 8831 | 0 | — | — | — | -| b300_sxm | `FLASHINFER_MLA` | shared | vllm | vllm:8832 | 0 / 8832 | 0 | — | — | — | +| b300_sxm | `FLASHINFER_MLA` | shared | vllm | vllm:20232 | 0 / 11400 | 0 | — | — | — | | b300_sxm | `default` | shared_fallback | trtllm | trtllm:8832 | 0 / 8832 | 0 | — | — | — | -| gb200 | `FLASHINFER_MLA` | shared | vllm | vllm:8832 | 0 / 8832 | 0 | — | — | — | +| gb200 | `FLASHINFER_MLA` | shared | vllm | vllm:20232 | 0 / 11400 | 0 | — | — | — | | gb200 | `default` | shared_fallback | trtllm | trtllm:8832 | 0 / 8832 | 0 | — | — | — | -| gb300 | `FLASHINFER_MLA` | shared | vllm | vllm:30812 | 0 / 22080 | 0 | — | — | — | +| gb300 | `FLASHINFER_MLA` | shared | vllm | vllm:42212 | 0 / 24648 | 30 | — | — | — | | gb300 | `default` | shared_fallback | trtllm | trtllm:8832 | 0 / 8832 | 0 | — | — | — | -| h100_sxm | `FLASHMLA` | shared | vllm | vllm:2944 | 0 / 2944 | 0 | — | — | — | -| h100_sxm | `FLASH_ATTN_MLA` | shared | vllm | vllm:2896 | 0 / 2896 | 0 | — | — | — | +| h100_sxm | `FLASHMLA` | shared | vllm | vllm:7360 | 0 / 4416 | 0 | — | — | — | +| h100_sxm | `FLASH_ATTN_MLA` | shared | vllm | vllm:7240 | 0 / 4344 | 0 | — | — | — | | h100_sxm | `default` | shared_fallback | trtllm | trtllm:5888 | 0 / 5888 | 0 | — | — | — | -| h200_sxm | `FLASHMLA` | shared | vllm | vllm:2944 | 0 / 2944 | 0 | — | — | — | -| h200_sxm | `FLASH_ATTN_MLA` | shared | vllm | vllm:2944 | 0 / 2944 | 0 | — | — | — | +| h200_sxm | `FLASHMLA` | shared | vllm | vllm:7360 | 0 / 4416 | 0 | — | — | — | +| h200_sxm | `FLASH_ATTN_MLA` | shared | vllm | vllm:7360 | 0 / 4416 | 0 | — | — | — | | h200_sxm | `default` | shared_fallback | trtllm | trtllm:5888 | 0 / 5888 | 0 | — | — | — | | l40s | `TRITON_MLA` | shared | vllm | vllm:2656 | 0 / 2656 | 0 | — | — | — | | rtx_pro_6000_server | `TRITON_MLA` | shared | vllm | vllm:2990 | 0 / 2990 | 0 | — | — | — | @@ -980,50 +1027,56 @@ Rows with a blank/`` kernel_source are skipped during the scan (the cur | a100_sxm | `sglang_marlin_moe` | shared | sglang | sglang:19035 | 0 / 16686 | 0 | — | — | — | | a100_sxm | `vllm_fused_moe` | shared | vllm | vllm:6360 | 0 / 6360 | 0 | — | — | — | | b200_sxm | `deepgemm` | shared | trtllm | trtllm:38799 | 0 / 38799 | 0 | — | — | — | -| b200_sxm | `moe_torch_flow` | shared | trtllm | trtllm:10692 | 0 / 6480 | 5 | — | — | — | +| b200_sxm | `moe_torch_flow` | shared | trtllm | trtllm:10854 | 0 / 10854 | 0 | — | — | — | | b200_sxm | `moe_torch_flow_cutlass` | shared | trtllm | trtllm:114048 | 0 / 114048 | 0 | — | — | — | | b200_sxm | `moe_torch_flow_min_latency` | shared | trtllm | trtllm:36045 | 0 / 36045 | 0 | — | — | — | | b200_sxm | `moe_torch_flow_nongated` | shared | trtllm | trtllm:22923 | 0 / 22923 | 0 | — | — | — | -| b200_sxm | `sglang_flashinfer_trtllm_moe` | shared | sglang | sglang:118503 | 0 / 118503 | 0 | — | — | — | -| b200_sxm | `sglang_fused_moe_triton` | shared | sglang | sglang:30942 | 0 / 30942 | 0 | — | — | — | -| b200_sxm | `sglang_mxfp4_flashinfer_trtllm_moe` | shared | sglang | sglang:5184 | 0 / 5184 | 0 | — | — | — | +| b200_sxm | `sglang_flashinfer_trtllm_moe` | shared | sglang | sglang:124821 | 0 / 124821 | 0 | — | — | — | +| b200_sxm | `sglang_fused_moe_triton` | shared | sglang | sglang:36936 | 0 / 36936 | 0 | — | — | — | +| b200_sxm | `sglang_mxfp4_flashinfer_trtllm_moe` | shared | sglang | sglang:5214 | 0 / 5214 | 0 | — | — | — | | b200_sxm | `vllm_compressedtensorsw4a4mxfp4moe_marlin_marlinexperts` | shared | vllm | vllm:972 | 0 / 972 | 0 | — | — | — | -| b200_sxm | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:972 | 0 / 972 | 0 | — | — | — | -| b200_sxm | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_trtllm_trtllmnvfp4expertsmodular` | shared | vllm | vllm:2754 | 0 / 2754 | 0 | — | — | — | -| b200_sxm | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_trtllm_trtllmnvfp4expertsmonolithic` | shared | vllm | vllm:15049 | 0 / 15049 | 0 | — | — | — | -| b200_sxm | `vllm_compressedtensorswna16marlinmoe_marlin_marlinexperts` | shared | vllm | vllm:1053 | 0 / 1053 | 0 | — | — | — | +| b200_sxm | `vllm_compressedtensorsw4a4mxfp4moe_marlin_marlinexperts_situ_as_silu` | shared | vllm | vllm:972 | 0 / 972 | 0 | — | — | — | +| b200_sxm | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:1944 | 0 / 972 | 0 | — | — | — | +| b200_sxm | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_trtllm_trtllmnvfp4expertsmodular` | shared | vllm | vllm:6156 | 0 / 3402 | 4 | — | — | — | +| b200_sxm | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_trtllm_trtllmnvfp4expertsmonolithic` | shared | vllm | vllm:29786 | 0 / 15101 | 13 | — | — | — | +| b200_sxm | `vllm_compressedtensorswna16marlinmoe_marlin_marlinexperts` | shared | vllm | vllm:2106 | 0 / 1053 | 0 | — | — | — | | b200_sxm | `vllm_flashinfer_trtllm_moe_fp4` | shared | vllm | vllm:10287 | 0 / 9234 | 0 | — | — | — | -| b200_sxm | `vllm_fp8moe_flashinfer_trtllm_trtllmfp8expertsmodular` | shared | vllm | vllm:3807 | 0 / 3807 | 0 | — | — | — | -| b200_sxm | `vllm_fp8moe_flashinfer_trtllm_trtllmfp8expertsmonolithic` | shared | vllm | vllm:11421 | 0 / 11421 | 0 | — | — | — | +| b200_sxm | `vllm_fp8moe_flashinfer_trtllm_trtllmfp8expertsmodular` | shared | vllm | vllm:8181 | 0 / 4374 | 1 | — | — | — | +| b200_sxm | `vllm_fp8moe_flashinfer_trtllm_trtllmfp8expertsmonolithic` | shared | vllm | vllm:24948 | 0 / 12474 | 12 | — | — | — | | b200_sxm | `vllm_fused_moe` | shared | vllm | vllm:83407 | 0 / 80329 | 0 | — | — | — | -| b200_sxm | `vllm_gptossmxfp4moe_flashinfer_trtllm_mxfp4_bf16_trtllmmxfp4expertsmonolithic` | shared | vllm | vllm:1944 | 0 / 1944 | 0 | — | — | — | +| b200_sxm | `vllm_gptossmxfp4moe_flashinfer_trtllm_mxfp4_bf16_trtllmmxfp4expertsmonolithic` | shared | vllm | vllm:3888 | 0 / 1944 | 6 | — | — | — | | b200_sxm | `vllm_marlin_int4_moe` | shared | vllm | vllm:10209 | 0 / 9237 | 0 | — | — | — | -| b200_sxm | `vllm_modeloptfp8moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:972 | 0 / 972 | 0 | — | — | — | -| b200_sxm | `vllm_modeloptfp8moe_flashinfer_trtllm_trtllmfp8expertsmonolithic` | shared | vllm | vllm:12304 | 0 / 12304 | 0 | — | — | — | +| b200_sxm | `vllm_modeloptfp8moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:2835 | 0 / 1863 | 1 | — | — | — | +| b200_sxm | `vllm_modeloptfp8moe_flashinfer_trtllm_trtllmfp8expertsmonolithic` | shared | vllm | vllm:24573 | 0 / 12503 | 17 | — | — | — | | b200_sxm | `vllm_mxfp4_moe` | shared | vllm | vllm:1138 | 0 / 1138 | 0 | — | — | — | -| b200_sxm | `vllm_mxfp4moe_flashinfer_trtllm_mxfp4_mxfp8_trtllmmxfp4expertsmodular` | shared | vllm | vllm:2187 | 0 / 2187 | 0 | — | — | — | -| b200_sxm | `vllm_unquantizedfusedmoe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:1944 | 0 / 1944 | 0 | — | — | — | -| b200_sxm | `vllm_unquantizedfusedmoe_flashinfer_trtllm_trtllmbf16experts` | shared | vllm | vllm:12424 | 0 / 12424 | 0 | — | — | — | +| b200_sxm | `vllm_mxfp4moe_flashinfer_trtllm_mxfp4_mxfp8_trtllmmxfp4expertsmodular` | shared | vllm | vllm:4374 | 0 / 2187 | 3 | — | — | — | +| b200_sxm | `vllm_unquantizedfusedmoe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:3726 | 0 / 2835 | 0 | — | — | — | +| b200_sxm | `vllm_unquantizedfusedmoe_flashinfer_trtllm_trtllmbf16experts` | shared | vllm | vllm:25720 | 0 / 13556 | 9 | — | — | — | +| b200_sxm | `vllm_unquantizedfusedmoe_flashinfer_trtllm_trtllmbf16expertsmonolithic` | shared | vllm | vllm:1053 | 0 / 1053 | 0 | — | — | — | +| b200_sxm | `vllm_unquantizedfusedmoe_triton_tritonexperts` | shared | vllm | vllm:1053 | 0 / 1053 | 0 | — | — | — | | b300_sxm | `deepgemm` | shared | trtllm | trtllm:38799 | 0 / 38799 | 0 | — | — | — | | b300_sxm | `moe_torch_flow` | shared | trtllm | trtllm:12472 | 0 / 7371 | 9 | — | — | — | | b300_sxm | `moe_torch_flow_cutlass` | shared | trtllm | trtllm:114048 | 0 / 114048 | 0 | — | — | — | | b300_sxm | `moe_torch_flow_nongated` | shared | trtllm | trtllm:22923 | 0 / 22923 | 0 | — | — | — | -| b300_sxm | `sglang_flashinfer_trtllm_moe` | shared | sglang | sglang:195448 | 0 / 115425 | 0 | — | — | — | -| b300_sxm | `sglang_fused_moe_triton` | shared | sglang | sglang:55588 | 0 / 30942 | 0 | — | — | — | +| b300_sxm | `sglang_flashinfer_trtllm_moe` | shared | sglang | sglang:198607 | 0 / 118584 | 0 | — | — | — | +| b300_sxm | `sglang_fused_moe_triton` | shared | sglang | sglang:61582 | 0 / 36936 | 0 | — | — | — | | b300_sxm | `sglang_mxfp4_flashinfer_trtllm_moe` | shared | sglang | sglang:9720 | 0 / 5184 | 0 | — | — | — | | b300_sxm | `vllm_compressedtensorsw4a4mxfp4moe_marlin_marlinexperts` | shared | vllm | vllm:972 | 0 / 972 | 0 | — | — | — | -| b300_sxm | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:972 | 0 / 972 | 0 | — | — | — | -| b300_sxm | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_trtllm_trtllmnvfp4expertsmodular` | shared | vllm | vllm:648 | 0 / 648 | 0 | — | — | — | -| b300_sxm | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_trtllm_trtllmnvfp4expertsmonolithic` | shared | vllm | vllm:13996 | 0 / 13996 | 0 | — | — | — | -| b300_sxm | `vllm_compressedtensorswna16marlinmoe_marlin_marlinexperts` | shared | vllm | vllm:1053 | 0 / 1053 | 0 | — | — | — | -| b300_sxm | `vllm_fp8moe_flashinfer_trtllm_trtllmfp8expertsmodular` | shared | vllm | vllm:3807 | 0 / 3807 | 0 | — | — | — | -| b300_sxm | `vllm_fp8moe_flashinfer_trtllm_trtllmfp8expertsmonolithic` | shared | vllm | vllm:11421 | 0 / 11421 | 0 | — | — | — | -| b300_sxm | `vllm_gptossmxfp4moe_flashinfer_trtllm_mxfp4_bf16_trtllmmxfp4expertsmonolithic` | shared | vllm | vllm:1944 | 0 / 1944 | 0 | — | — | — | -| b300_sxm | `vllm_modeloptfp8moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:972 | 0 / 972 | 0 | — | — | — | -| b300_sxm | `vllm_modeloptfp8moe_flashinfer_trtllm_trtllmfp8expertsmonolithic` | shared | vllm | vllm:11266 | 0 / 11266 | 0 | — | — | — | -| b300_sxm | `vllm_mxfp4moe_flashinfer_trtllm_mxfp4_mxfp8_trtllmmxfp4expertsmodular` | shared | vllm | vllm:2187 | 0 / 2187 | 0 | — | — | — | -| b300_sxm | `vllm_unquantizedfusedmoe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:1944 | 0 / 1944 | 0 | — | — | — | -| b300_sxm | `vllm_unquantizedfusedmoe_flashinfer_trtllm_trtllmbf16experts` | shared | vllm | vllm:12224 | 0 / 12224 | 0 | — | — | — | +| b300_sxm | `vllm_compressedtensorsw4a4mxfp4moe_marlin_marlinexperts_situ_as_silu` | shared | vllm | vllm:972 | 0 / 972 | 0 | — | — | — | +| b300_sxm | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:1944 | 0 / 972 | 0 | — | — | — | +| b300_sxm | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_trtllm_trtllmnvfp4expertsmodular` | shared | vllm | vllm:4050 | 0 / 3402 | 0 | — | — | — | +| b300_sxm | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_trtllm_trtllmnvfp4expertsmonolithic` | shared | vllm | vllm:28785 | 0 / 15127 | 23 | — | — | — | +| b300_sxm | `vllm_compressedtensorswna16marlinmoe_marlin_marlinexperts` | shared | vllm | vllm:2106 | 0 / 1053 | 0 | — | — | — | +| b300_sxm | `vllm_fp8moe_flashinfer_trtllm_trtllmfp8expertsmodular` | shared | vllm | vllm:8181 | 0 / 4374 | 5 | — | — | — | +| b300_sxm | `vllm_fp8moe_flashinfer_trtllm_trtllmfp8expertsmonolithic` | shared | vllm | vllm:24948 | 0 / 12474 | 21 | — | — | — | +| b300_sxm | `vllm_gptossmxfp4moe_flashinfer_trtllm_mxfp4_bf16_trtllmmxfp4expertsmonolithic` | shared | vllm | vllm:3888 | 0 / 1944 | 5 | — | — | — | +| b300_sxm | `vllm_modeloptfp8moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:2835 | 0 / 1863 | 1 | — | — | — | +| b300_sxm | `vllm_modeloptfp8moe_flashinfer_trtllm_trtllmfp8expertsmonolithic` | shared | vllm | vllm:23821 | 0 / 12555 | 13 | — | — | — | +| b300_sxm | `vllm_mxfp4moe_flashinfer_trtllm_mxfp4_mxfp8_trtllmmxfp4expertsmodular` | shared | vllm | vllm:4374 | 0 / 2187 | 3 | — | — | — | +| b300_sxm | `vllm_unquantizedfusedmoe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:3726 | 0 / 2835 | 1 | — | — | — | +| b300_sxm | `vllm_unquantizedfusedmoe_flashinfer_trtllm_trtllmbf16experts` | shared | vllm | vllm:25728 | 0 / 13582 | 15 | — | — | — | +| b300_sxm | `vllm_unquantizedfusedmoe_flashinfer_trtllm_trtllmbf16expertsmonolithic` | shared | vllm | vllm:1053 | 0 / 1053 | 0 | — | — | — | +| b300_sxm | `vllm_unquantizedfusedmoe_triton_tritonexperts` | shared | vllm | vllm:1053 | 0 / 1053 | 0 | — | — | — | | b60 | `vllm_xpu_moe` | shared | vllm | vllm:1836 | 0 / 1404 | 0 | — | — | — | | b60 | `vllm_xpu_moe_mxfp4` | shared | vllm | vllm:2592 | 0 / 1296 | 0 | — | — | — | | gb200 | `deepgemm` | shared | trtllm | trtllm:38799 | 0 / 38799 | 0 | — | — | — | @@ -1031,73 +1084,79 @@ Rows with a blank/`` kernel_source are skipped during the scan (the cur | gb200 | `moe_torch_flow_cutlass` | shared | trtllm | trtllm:114048 | 0 / 114048 | 0 | — | — | — | | gb200 | `moe_torch_flow_min_latency` | shared | trtllm | trtllm:36045 | 0 / 36045 | 0 | — | — | — | | gb200 | `moe_torch_flow_nongated` | shared | trtllm | trtllm:22923 | 0 / 22923 | 0 | — | — | — | -| gb200 | `sglang_flashinfer_trtllm_moe` | shared | sglang | sglang:153054 | 0 / 118500 | 0 | — | — | — | -| gb200 | `sglang_fused_moe_triton` | shared | sglang | sglang:44631 | 0 / 30942 | 0 | — | — | — | -| gb200 | `sglang_mxfp4_flashinfer_trtllm_moe` | shared | sglang | sglang:7533 | 0 / 5184 | 0 | — | — | — | -| gb200 | `vllm_compressedtensorsw4a4mxfp4moe_marlin_marlinexperts_situ_as_silu` | shared | vllm | vllm:972 | 0 / 972 | 0 | — | — | — | -| gb200 | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:972 | 0 / 972 | 0 | — | — | — | -| gb200 | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_trtllm_trtllmnvfp4expertsmodular` | shared | vllm | vllm:2754 | 0 / 2754 | 0 | — | — | — | -| gb200 | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_trtllm_trtllmnvfp4expertsmonolithic` | shared | vllm | vllm:15075 | 0 / 15075 | 0 | — | — | — | -| gb200 | `vllm_compressedtensorswna16marlinmoe_marlin_marlinexperts` | shared | vllm | vllm:1053 | 0 / 1053 | 0 | — | — | — | -| gb200 | `vllm_fp8moe_flashinfer_trtllm_trtllmfp8expertsmodular` | shared | vllm | vllm:3807 | 0 / 3807 | 0 | — | — | — | -| gb200 | `vllm_fp8moe_flashinfer_trtllm_trtllmfp8expertsmonolithic` | shared | vllm | vllm:11421 | 0 / 11421 | 0 | — | — | — | -| gb200 | `vllm_gptossmxfp4moe_flashinfer_trtllm_mxfp4_bf16_trtllmmxfp4expertsmonolithic` | shared | vllm | vllm:1944 | 0 / 1944 | 0 | — | — | — | -| gb200 | `vllm_modeloptfp8moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:972 | 0 / 972 | 0 | — | — | — | -| gb200 | `vllm_modeloptfp8moe_flashinfer_trtllm_trtllmfp8expertsmonolithic` | shared | vllm | vllm:11138 | 0 / 11138 | 0 | — | — | — | -| gb200 | `vllm_mxfp4moe_flashinfer_trtllm_mxfp4_mxfp8_trtllmmxfp4expertsmodular` | shared | vllm | vllm:2187 | 0 / 2187 | 0 | — | — | — | -| gb200 | `vllm_unquantizedfusedmoe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:1944 | 0 / 1944 | 0 | — | — | — | -| gb200 | `vllm_unquantizedfusedmoe_flashinfer_trtllm_trtllmbf16experts` | shared | vllm | vllm:12320 | 0 / 12320 | 0 | — | — | — | +| gb200 | `sglang_flashinfer_trtllm_moe` | shared | sglang | sglang:159372 | 0 / 124818 | 0 | — | — | — | +| gb200 | `sglang_fused_moe_triton` | shared | sglang | sglang:50625 | 0 / 36936 | 0 | — | — | — | +| gb200 | `sglang_mxfp4_flashinfer_trtllm_moe` | shared | sglang | sglang:7563 | 0 / 5214 | 0 | — | — | — | +| gb200 | `vllm_compressedtensorsw4a4mxfp4moe_marlin_marlinexperts_situ_as_silu` | shared | vllm | vllm:1944 | 0 / 972 | 0 | — | — | — | +| gb200 | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:1944 | 0 / 972 | 0 | — | — | — | +| gb200 | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_trtllm_trtllmnvfp4expertsmodular` | shared | vllm | vllm:6156 | 0 / 3402 | 5 | — | — | — | +| gb200 | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_trtllm_trtllmnvfp4expertsmonolithic` | shared | vllm | vllm:29864 | 0 / 15179 | 23 | — | — | — | +| gb200 | `vllm_compressedtensorswna16marlinmoe_marlin_marlinexperts` | shared | vllm | vllm:2106 | 0 / 1053 | 0 | — | — | — | +| gb200 | `vllm_fp8moe_flashinfer_trtllm_trtllmfp8expertsmodular` | shared | vllm | vllm:8181 | 0 / 4374 | 2 | — | — | — | +| gb200 | `vllm_fp8moe_flashinfer_trtllm_trtllmfp8expertsmonolithic` | shared | vllm | vllm:24948 | 0 / 12474 | 13 | — | — | — | +| gb200 | `vllm_gptossmxfp4moe_flashinfer_trtllm_mxfp4_bf16_trtllmmxfp4expertsmonolithic` | shared | vllm | vllm:3888 | 0 / 1944 | 1 | — | — | — | +| gb200 | `vllm_modeloptfp8moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:2835 | 0 / 1863 | 1 | — | — | — | +| gb200 | `vllm_modeloptfp8moe_flashinfer_trtllm_trtllmfp8expertsmonolithic` | shared | vllm | vllm:23693 | 0 / 12555 | 21 | — | — | — | +| gb200 | `vllm_mxfp4moe_flashinfer_trtllm_mxfp4_mxfp8_trtllmmxfp4expertsmodular` | shared | vllm | vllm:4374 | 0 / 2187 | 1 | — | — | — | +| gb200 | `vllm_unquantizedfusedmoe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:3726 | 0 / 2835 | 3 | — | — | — | +| gb200 | `vllm_unquantizedfusedmoe_flashinfer_trtllm_trtllmbf16experts` | shared | vllm | vllm:25876 | 0 / 13556 | 12 | — | — | — | +| gb200 | `vllm_unquantizedfusedmoe_flashinfer_trtllm_trtllmbf16expertsmonolithic` | shared | vllm | vllm:1026 | 0 / 1026 | 0 | — | — | — | +| gb200 | `vllm_unquantizedfusedmoe_triton_tritonexperts` | shared | vllm | vllm:1053 | 0 / 1053 | 0 | — | — | — | | gb300 | `deepgemm` | shared | trtllm | trtllm:38799 | 0 / 38799 | 0 | — | — | — | | gb300 | `moe_torch_flow` | shared | trtllm | trtllm:12474 | 0 / 7371 | 6 | — | — | — | | gb300 | `moe_torch_flow_cutlass` | shared | trtllm | trtllm:114048 | 0 / 114048 | 0 | — | — | — | | gb300 | `moe_torch_flow_nongated` | shared | trtllm | trtllm:22923 | 0 / 22923 | 0 | — | — | — | -| gb300 | `sglang_flashinfer_trtllm_moe` | shared | sglang | sglang:156186 | 0 / 118500 | 0 | — | — | — | -| gb300 | `sglang_fused_moe_triton` | shared | sglang | sglang:44633 | 0 / 30942 | 0 | — | — | — | +| gb300 | `sglang_flashinfer_trtllm_moe` | shared | sglang | sglang:162504 | 0 / 124818 | 0 | — | — | — | +| gb300 | `sglang_fused_moe_triton` | shared | sglang | sglang:50627 | 0 / 36936 | 0 | — | — | — | | gb300 | `sglang_mxfp4_flashinfer_trtllm_moe` | shared | sglang | sglang:7533 | 0 / 5184 | 0 | — | — | — | -| gb300 | `vllm_compressedtensorsw4a4mxfp4moe_marlin_marlinexperts_situ_as_silu` | shared | vllm | vllm:972 | 0 / 972 | 0 | — | — | — | -| gb300 | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:972 | 0 / 972 | 0 | — | — | — | -| gb300 | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_trtllm_trtllmnvfp4expertsmodular` | shared | vllm | vllm:648 | 0 / 648 | 0 | — | — | — | -| gb300 | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_trtllm_trtllmnvfp4expertsmonolithic` | shared | vllm | vllm:13996 | 0 / 13996 | 0 | — | — | — | -| gb300 | `vllm_compressedtensorswna16marlinmoe_marlin_marlinexperts` | shared | vllm | vllm:1053 | 0 / 1053 | 0 | — | — | — | -| gb300 | `vllm_fp8moe_flashinfer_trtllm_trtllmfp8expertsmodular` | shared | vllm | vllm:3807 | 0 / 3807 | 0 | — | — | — | -| gb300 | `vllm_fp8moe_flashinfer_trtllm_trtllmfp8expertsmonolithic` | shared | vllm | vllm:11421 | 0 / 11421 | 0 | — | — | — | -| gb300 | `vllm_gptossmxfp4moe_flashinfer_trtllm_mxfp4_bf16_trtllmmxfp4expertsmonolithic` | shared | vllm | vllm:1944 | 0 / 1944 | 0 | — | — | — | -| gb300 | `vllm_modeloptfp8moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:972 | 0 / 972 | 0 | — | — | — | -| gb300 | `vllm_modeloptfp8moe_flashinfer_trtllm_trtllmfp8expertsmonolithic` | shared | vllm | vllm:11295 | 0 / 11295 | 0 | — | — | — | -| gb300 | `vllm_mxfp4moe_flashinfer_trtllm_mxfp4_mxfp8_trtllmmxfp4expertsmodular` | shared | vllm | vllm:2187 | 0 / 2187 | 0 | — | — | — | -| gb300 | `vllm_unquantizedfusedmoe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:1944 | 0 / 1944 | 0 | — | — | — | -| gb300 | `vllm_unquantizedfusedmoe_flashinfer_trtllm_trtllmbf16experts` | shared | vllm | vllm:12267 | 0 / 12267 | 0 | — | — | — | +| gb300 | `vllm_compressedtensorsw4a4mxfp4moe_marlin_marlinexperts_situ_as_silu` | shared | vllm | vllm:1944 | 0 / 972 | 0 | — | — | — | +| gb300 | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:1944 | 0 / 972 | 0 | — | — | — | +| gb300 | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_trtllm_trtllmnvfp4expertsmodular` | shared | vllm | vllm:4050 | 0 / 3402 | 0 | — | — | — | +| gb300 | `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_trtllm_trtllmnvfp4expertsmonolithic` | shared | vllm | vllm:28785 | 0 / 15127 | 23 | — | — | — | +| gb300 | `vllm_compressedtensorswna16marlinmoe_marlin_marlinexperts` | shared | vllm | vllm:2106 | 0 / 1053 | 0 | — | — | — | +| gb300 | `vllm_fp8moe_flashinfer_trtllm_trtllmfp8expertsmodular` | shared | vllm | vllm:8181 | 0 / 4374 | 4 | — | — | — | +| gb300 | `vllm_fp8moe_flashinfer_trtllm_trtllmfp8expertsmonolithic` | shared | vllm | vllm:24948 | 0 / 12474 | 9 | — | — | — | +| gb300 | `vllm_gptossmxfp4moe_flashinfer_trtllm_mxfp4_bf16_trtllmmxfp4expertsmonolithic` | shared | vllm | vllm:3888 | 0 / 1944 | 4 | — | — | — | +| gb300 | `vllm_modeloptfp8moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:2835 | 0 / 1863 | 2 | — | — | — | +| gb300 | `vllm_modeloptfp8moe_flashinfer_trtllm_trtllmfp8expertsmonolithic` | shared | vllm | vllm:23850 | 0 / 12555 | 20 | — | — | — | +| gb300 | `vllm_mxfp4moe_flashinfer_trtllm_mxfp4_mxfp8_trtllmmxfp4expertsmodular` | shared | vllm | vllm:4374 | 0 / 2187 | 4 | — | — | — | +| gb300 | `vllm_unquantizedfusedmoe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:3726 | 0 / 2835 | 3 | — | — | — | +| gb300 | `vllm_unquantizedfusedmoe_flashinfer_trtllm_trtllmbf16experts` | shared | vllm | vllm:25849 | 0 / 13608 | 15 | — | — | — | +| gb300 | `vllm_unquantizedfusedmoe_flashinfer_trtllm_trtllmbf16expertsmonolithic` | shared | vllm | vllm:1026 | 0 / 1026 | 0 | — | — | — | +| gb300 | `vllm_unquantizedfusedmoe_triton_tritonexperts` | shared | vllm | vllm:1053 | 0 / 1053 | 0 | — | — | — | | h100_sxm | `moe_torch_flow` | shared | trtllm | trtllm:1539 | 0 / 1053 | 1 | — | — | — | | h100_sxm | `moe_torch_flow_cutlass` | shared | trtllm | trtllm:185328 | 0 / 129357 | 16 | — | — | — | | h100_sxm | `moe_torch_flow_nongated` | shared | trtllm | trtllm:32643 | 0 / 24462 | 9 | — | — | — | -| h100_sxm | `sglang_flashinfer_cutlass_moe` | shared | sglang | sglang:8586 | 0 / 8586 | 0 | — | — | — | +| h100_sxm | `sglang_flashinfer_cutlass_moe` | shared | sglang | sglang:8601 | 0 / 8601 | 0 | — | — | — | | h100_sxm | `sglang_fused_moe_triton` | shared | sglang | sglang:157464 | 0 / 112509 | 1 | — | — | — | | h100_sxm | `sglang_marlin_moe` | shared | sglang | sglang:22113 | 0 / 17415 | 0 | — | — | — | | h100_sxm | `sglang_marlin_moe_situ_as_silu` | shared | sglang | sglang:541 | 0 / 541 | 0 | — | — | — | -| h100_sxm | `vllm_compressedtensorsw4a4mxfp4moe_marlin_marlinexperts_situ_as_silu` | shared | vllm | vllm:972 | 0 / 972 | 0 | — | — | — | -| h100_sxm | `vllm_compressedtensorswna16marlinmoe_marlin_marlinexperts` | shared | vllm | vllm:1053 | 0 / 1053 | 0 | — | — | — | -| h100_sxm | `vllm_fp8moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:9234 | 0 / 9234 | 0 | — | — | — | -| h100_sxm | `vllm_fp8moe_triton_tritonexperts` | shared | vllm | vllm:5988 | 0 / 5988 | 0 | — | — | — | -| h100_sxm | `vllm_gptossmxfp4moe_triton_oaitritonmxfp4expertsmonolithic` | shared | vllm | vllm:1944 | 0 / 1944 | 0 | — | — | — | -| h100_sxm | `vllm_modeloptfp8moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:14418 | 0 / 14418 | 0 | — | — | — | -| h100_sxm | `vllm_unquantizedfusedmoe_triton_tritonexperts` | shared | vllm | vllm:16253 | 0 / 16253 | 0 | — | — | — | +| h100_sxm | `sglang_mxfp4_humming_moe` | shared | sglang | sglang:15 | 0 / 15 | 0 | — | — | — | +| h100_sxm | `vllm_compressedtensorsw4a4mxfp4moe_marlin_marlinexperts_situ_as_silu` | shared | vllm | vllm:1944 | 0 / 972 | 0 | — | — | — | +| h100_sxm | `vllm_compressedtensorswna16marlinmoe_marlin_marlinexperts` | shared | vllm | vllm:2106 | 0 / 1053 | 0 | — | — | — | +| h100_sxm | `vllm_fp8moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:19521 | 0 / 10287 | 3 | — | — | — | +| h100_sxm | `vllm_fp8moe_triton_tritonexperts` | shared | vllm | vllm:12549 | 0 / 6561 | 3 | — | — | — | +| h100_sxm | `vllm_gptossmxfp4moe_triton_oaitritonmxfp4expertsmonolithic` | shared | vllm | vllm:3888 | 0 / 1944 | 4 | — | — | — | +| h100_sxm | `vllm_modeloptfp8moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:29727 | 0 / 15309 | 4 | — | — | — | +| h100_sxm | `vllm_unquantizedfusedmoe_triton_tritonexperts` | shared | vllm | vllm:34559 | 0 / 18306 | 11 | — | — | — | | h200_sxm | `moe_torch_flow` | shared | trtllm | trtllm:2268 | 0 / 1134 | 1 | — | — | — | | h200_sxm | `moe_torch_flow_cutlass` | shared | trtllm | trtllm:176580 | 0 / 129357 | 12 | — | — | — | | h200_sxm | `moe_torch_flow_nongated` | shared | trtllm | trtllm:32724 | 0 / 24543 | 14 | — | — | — | -| h200_sxm | `sglang_flashinfer_cutlass_moe` | shared | sglang | sglang:8667 | 0 / 8667 | 0 | — | — | — | +| h200_sxm | `sglang_flashinfer_cutlass_moe` | shared | sglang | sglang:8682 | 0 / 8682 | 0 | — | — | — | | h200_sxm | `sglang_fused_moe_triton` | shared | sglang | sglang:157464 | 0 / 112509 | 1 | — | — | — | | h200_sxm | `sglang_marlin_moe` | shared | sglang | sglang:22113 | 0 / 17415 | 0 | — | — | — | | h200_sxm | `sglang_marlin_moe_situ_as_silu` | shared | sglang | sglang:488 | 0 / 488 | 0 | — | — | — | -| h200_sxm | `vllm_compressedtensorsw4a4mxfp4moe_marlin_marlinexperts_situ_as_silu` | shared | vllm | vllm:972 | 0 / 972 | 0 | — | — | — | -| h200_sxm | `vllm_compressedtensorswna16marlinmoe_marlin_marlinexperts` | shared | vllm | vllm:1053 | 0 / 1053 | 0 | — | — | — | -| h200_sxm | `vllm_fp8moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:9234 | 0 / 9234 | 0 | — | — | — | -| h200_sxm | `vllm_fp8moe_triton_tritonexperts` | shared | vllm | vllm:5994 | 0 / 5994 | 0 | — | — | — | +| h200_sxm | `sglang_mxfp4_humming_moe` | shared | sglang | sglang:15 | 0 / 15 | 0 | — | — | — | +| h200_sxm | `vllm_compressedtensorsw4a4mxfp4moe_marlin_marlinexperts_situ_as_silu` | shared | vllm | vllm:1944 | 0 / 972 | 0 | — | — | — | +| h200_sxm | `vllm_compressedtensorswna16marlinmoe_marlin_marlinexperts` | shared | vllm | vllm:2106 | 0 / 1053 | 0 | — | — | — | +| h200_sxm | `vllm_fp8moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:19521 | 0 / 10287 | 14 | — | — | — | +| h200_sxm | `vllm_fp8moe_triton_tritonexperts` | shared | vllm | vllm:12555 | 0 / 6561 | 0 | — | — | — | | h200_sxm | `vllm_fused_moe` | shared | vllm | vllm:36288 | 0 / 33210 | 0 | — | — | — | -| h200_sxm | `vllm_gptossmxfp4moe_triton_oaitritonmxfp4expertsmonolithic` | shared | vllm | vllm:1944 | 0 / 1944 | 0 | — | — | — | +| h200_sxm | `vllm_gptossmxfp4moe_triton_oaitritonmxfp4expertsmonolithic` | shared | vllm | vllm:3888 | 0 / 1944 | 3 | — | — | — | | h200_sxm | `vllm_marlin_int4_moe` | shared | vllm | vllm:10206 | 0 / 9234 | 0 | — | — | — | -| h200_sxm | `vllm_modeloptfp8moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:14418 | 0 / 14418 | 0 | — | — | — | +| h200_sxm | `vllm_modeloptfp8moe_flashinfer_cutlass_flashinferexperts` | shared | vllm | vllm:29727 | 0 / 15309 | 5 | — | — | — | | h200_sxm | `vllm_mxfp4_moe` | shared | vllm | vllm:1701 | 0 / 1296 | 0 | — | — | — | -| h200_sxm | `vllm_unquantizedfusedmoe_triton_tritonexperts` | shared | vllm | vllm:16281 | 0 / 16281 | 0 | — | — | — | +| h200_sxm | `vllm_unquantizedfusedmoe_triton_tritonexperts` | shared | vllm | vllm:34587 | 0 / 18306 | 1 | — | — | — | | l40s | `moe_torch_flow` | shared | trtllm | trtllm:1960 | 0 / 1960 | 0 | — | — | — | | l40s | `moe_torch_flow_cutlass` | shared | trtllm | trtllm:77077 | 0 / 72092 | 7 | — | — | — | | l40s | `moe_torch_flow_nongated` | shared | trtllm | trtllm:14372 | 0 / 14372 | 0 | — | — | — | @@ -1126,22 +1185,22 @@ Rows with a blank/`` kernel_source are skipped during the scan (the cur | system | kernel_source | tier | frameworks | rows_per_fw | overlap_keys | dedup rows | median % | p95 % | max % | |---|---|---|---|---|---|---|---|---|---| -| b200_sxm | `MiniMaxM3SparseMSAImpl` | shared | vllm | vllm:11712 | 0 / 11712 | 0 | — | — | — | +| b200_sxm | `MiniMaxM3SparseMSAImpl` | shared | vllm | vllm:25822 | 0 / 14396 | 47 | — | — | — | | b200_sxm | `msa_fmha_sm100` | shared | trtllm | trtllm:5070 | 0 / 5070 | 0 | — | — | — | | b200_sxm | `sglang_minimax_prefill_triton_sparse` | shared | sglang | sglang:26933 | 0 / 26933 | 0 | — | — | — | -| b300_sxm | `MiniMaxM3SparseMSAImpl` | shared | vllm | vllm:11712 | 0 / 11712 | 0 | — | — | — | +| b300_sxm | `MiniMaxM3SparseMSAImpl` | shared | vllm | vllm:25849 | 0 / 14431 | 73 | — | — | — | | b300_sxm | `msa_fmha_sm100` | shared | trtllm | trtllm:5070 | 0 / 5070 | 0 | — | — | — | | b300_sxm | `sglang_minimax_prefill_triton_sparse` | shared | sglang | sglang:26850 | 0 / 26850 | 0 | — | — | — | -| gb200 | `MiniMaxM3SparseMSAImpl` | shared | vllm | vllm:11712 | 0 / 11712 | 0 | — | — | — | +| gb200 | `MiniMaxM3SparseMSAImpl` | shared | vllm | vllm:25820 | 0 / 14388 | 57 | — | — | — | | gb200 | `msa_fmha_sm100` | shared | trtllm | trtllm:5070 | 0 / 5070 | 0 | — | — | — | | gb200 | `sglang_minimax_prefill_triton_sparse` | shared | sglang | sglang:26608 | 0 / 26608 | 0 | — | — | — | -| gb300 | `MiniMaxM3SparseMSAImpl` | shared | vllm | vllm:11712 | 0 / 11712 | 0 | — | — | — | +| gb300 | `MiniMaxM3SparseMSAImpl` | shared | vllm | vllm:25886 | 0 / 14403 | 26 | — | — | — | | gb300 | `msa_fmha_sm100` | shared | trtllm | trtllm:5070 | 0 / 5070 | 0 | — | — | — | | gb300 | `sglang_minimax_prefill_triton_sparse` | shared | sglang | sglang:27242 | 0 / 27242 | 0 | — | — | — | -| h100_sxm | `MiniMaxM3SparseTritonImpl` | shared | vllm | vllm:7808 | 0 / 7808 | 0 | — | — | — | +| h100_sxm | `MiniMaxM3SparseTritonImpl` | shared | vllm | vllm:19078 | 0 / 11567 | 30 | — | — | — | | h100_sxm | `default` | shared_fallback | trtllm | trtllm:3382 | 0 / 3382 | 0 | — | — | — | | h100_sxm | `sglang_minimax_prefill_triton_sparse` | shared | sglang | sglang:26348 | 0 / 26348 | 0 | — | — | — | -| h200_sxm | `MiniMaxM3SparseTritonImpl` | shared | vllm | vllm:7808 | 0 / 7808 | 0 | — | — | — | +| h200_sxm | `MiniMaxM3SparseTritonImpl` | shared | vllm | vllm:19076 | 0 / 11567 | 144 | — | — | — | | h200_sxm | `default` | shared_fallback | trtllm | trtllm:3388 | 0 / 3388 | 0 | — | — | — | | h200_sxm | `sglang_minimax_prefill_triton_sparse` | shared | sglang | sglang:26845 | 0 / 26845 | 0 | — | — | — | | l40s | `MiniMaxM3SparseTritonImpl` | shared | vllm | vllm:7808 | 0 / 7808 | 0 | — | — | — | @@ -1323,60 +1382,62 @@ Rows with a blank/`` kernel_source are skipped during the scan (the cur Each row is one distinct `kernel_source` value seen in the corpus, with the union of frameworks, op files, and systems it appears in. Tier is determined by the kernel_source name alone, so a single kernel_source has one tier across the whole corpus. -### `shared` (163 kernel sources) +### `shared` (175 kernel sources) | kernel_source | frameworks | op files | systems | rows | |---|---|---|---|---| -| `causal_conv1d_fn` | sglang, trtllm, vllm | gdn_perf.parquet, mamba2_perf.parquet | a100_sxm, b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 77,008 | +| `causal_conv1d_fn` | sglang, trtllm, vllm | gdn_perf.parquet, mamba2_perf.parquet | a100_sxm, b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 109,550 | | `causal_conv1d_fn_qkv3` | sglang, vllm | kda_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 7,012 | -| `causal_conv1d_update` | sglang, trtllm, vllm | gdn_perf.parquet, kda_perf.parquet, mamba2_perf.parquet | a100_sxm, b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 9,896 | -| `chunk_gated_delta_rule` | sglang, trtllm | gdn_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 38,625 | -| `chunk_gated_delta_rule_flashinfer` | vllm | gdn_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 24,861 | +| `causal_conv1d_update` | sglang, trtllm, vllm | gdn_perf.parquet, kda_perf.parquet, mamba2_perf.parquet | a100_sxm, b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 13,098 | +| `chunk_gated_delta_rule` | sglang, trtllm | gdn_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 40,725 | +| `chunk_gated_delta_rule_flashinfer` | vllm | gdn_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 55,303 | | `chunk_gated_delta_rule_triton` | vllm | gdn_perf.parquet | l40s, rtx_pro_6000_server | 7,870 | | `chunk_kda` | sglang | kda_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 3,168 | | `chunk_kda_with_fused_gate` | vllm | kda_perf.parquet | l40s | 414 | | `compressed_flashmla` | sglang | dsv4_csa_context_module_perf.parquet, dsv4_csa_generation_module_perf.parquet, dsv4_hca_context_module_perf.parquet, dsv4_hca_generation_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, rtx_pro_6000_server | 748,644 | -| `CutlassFp8BlockScaledMMKernel` | vllm | gemm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, rtx_pro_6000_server | 28,934 | -| `CutlassFP8ScaledMMLinearKernel` | vllm | gemm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 285,912 | +| `CutlassFp8BlockScaledMMKernel` | vllm | gemm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, rtx_pro_6000_server | 51,726 | +| `CutlassFP8ScaledMMLinearKernel` | vllm | gemm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 654,580 | | `deep_gemm.fp8_mqa_logits` | sglang | glm5_mqa_logits_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 19,378 | | `deep_gemm.fp8_paged_mqa_logits` | sglang | dsv4_paged_mqa_logits_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 19,276 | | `deepep` | sglang, trtllm, vllm | moe_a2a_perf.parquet, wideep_deepep_ll_perf.parquet, wideep_deepep_normal_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 23,512 | | `deepepmoe` | sglang | wideep_context_moe_perf.parquet, wideep_generation_moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 6,333 | | `deepgemm` | trtllm | gemm_perf.parquet, moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 272,268 | | `deepgemm_megamoe` | sglang | dsv4_megamoe_module_perf.parquet | b200_sxm, gb200, gb300 | 776 | -| `DeepGemmFp8BlockScaledMMKernel` | vllm | gemm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 171,756 | +| `DeepGemmFp8BlockScaledMMKernel` | vllm | gemm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 296,224 | | `deepseek_v3` | sglang | wideep_context_mlp_perf.parquet, wideep_generation_mlp_perf.parquet | h100_sxm, h200_sxm | 72 | | `DeepseekV4TrtllmAttention` | trtllm | dsv4_csa_context_module_perf.parquet, dsv4_csa_generation_module_perf.parquet, dsv4_hca_context_module_perf.parquet, dsv4_hca_generation_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 57,096 | | `dynamic_per_token_scaled_fp8_quant_minus_static_scaled_fp8_quant` | vllm | computescale_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 12,941 | -| `fa3` | sglang | context_attention_perf.parquet, generation_attention_perf.parquet, wideep_context_mla_perf.parquet, wideep_generation_mla_perf.parquet | h100_sxm, h200_sxm | 142,844 | +| `fa3` | sglang | context_attention_perf.parquet, generation_attention_perf.parquet, wideep_context_mla_perf.parquet, wideep_generation_mla_perf.parquet | h100_sxm, h200_sxm | 145,130 | | `fast_topk_transform_fused` | sglang | glm5_topk_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 24,092 | -| `flash_attention` | sglang | context_attention_perf.parquet, context_mla_perf.parquet, generation_attention_perf.parquet, generation_mla_perf.parquet | a100_sxm, h100_sxm, h200_sxm | 24,688 | +| `flash_attention` | sglang | context_attention_perf.parquet, context_mla_perf.parquet, generation_attention_perf.parquet, generation_mla_perf.parquet | a100_sxm, h100_sxm, h200_sxm | 26,008 | | `flash_attention_v3` | sglang | encoder_attention_perf.parquet | h100_sxm, h200_sxm | 15,358 | | `flash_attention_v4` | sglang | encoder_attention_perf.parquet | b200_sxm, gb200 | 15,358 | -| `FLASH_ATTN` | vllm | mla_context_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 44,955 | -| `FLASH_ATTN_MLA` | vllm | mla_context_module_perf.parquet, mla_generation_module_perf.parquet | h100_sxm, h200_sxm | 7,856 | +| `FLASH_ATTN` | vllm | mla_context_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 84,027 | +| `FLASH_ATTN_MLA` | vllm | mla_context_module_perf.parquet, mla_generation_module_perf.parquet | h100_sxm, h200_sxm | 19,640 | | `flash_mla_sparse_fwd` | sglang | glm5_dsa_attn_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 19,841 | | `flashinfer` | sglang | context_attention_perf.parquet, generation_attention_perf.parquet, wideep_context_mla_perf.parquet, wideep_generation_mla_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 133,439 | -| `FLASHINFER_MLA` | vllm | mla_context_module_perf.parquet, mla_generation_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 61,516 | +| `flashinfer_gated_delta_rule_decode` | sglang | gdn_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 208 | +| `FLASHINFER_MLA` | vllm | mla_context_module_perf.parquet, mla_generation_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 110,536 | | `FLASHINFER_MLA_SPARSE` | vllm | dsa_context_module_perf.parquet, dsa_generation_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 71,728 | -| `FlashInferCuteDslNvFp4LinearKernel` | vllm | gemm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 142,968 | +| `FlashInferCuteDslNvFp4LinearKernel` | vllm | gemm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 436,600 | | `FlashInferCutlassNvFp4LinearKernel` | vllm | gemm_perf.parquet | rtx_pro_6000_server | 35,742 | -| `FlashInferFp8BlockScaledMMKernel` | vllm | gemm_perf.parquet | h100_sxm, h200_sxm | 14,241 | +| `FlashInferFp8BlockScaledMMKernel` | vllm | gemm_perf.parquet | h100_sxm, h200_sxm | 14,280 | | `flashkda_fwd` | vllm | kda_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, rtx_pro_6000_server | 3,415 | -| `FLASHMLA` | vllm | mla_context_module_perf.parquet, mla_generation_module_perf.parquet | h100_sxm, h200_sxm | 7,292 | +| `FLASHMLA` | vllm | mla_context_module_perf.parquet, mla_generation_module_perf.parquet | h100_sxm, h200_sxm | 18,212 | | `FLASHMLA_SPARSE` | vllm | dsa_context_module_perf.parquet, dsa_generation_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 83,700 | -| `FLASHMLA_SPARSE_DSV4` | vllm | dsv4_csa_context_module_perf.parquet, dsv4_csa_generation_module_perf.parquet, dsv4_hca_attn_module_perf.parquet, dsv4_hca_context_module_perf.parquet, dsv4_hca_generation_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 98,987 | +| `FLASHMLA_SPARSE_DSV4` | vllm | dsv4_csa_context_module_perf.parquet, dsv4_csa_generation_module_perf.parquet, dsv4_hca_attn_module_perf.parquet, dsv4_hca_context_module_perf.parquet, dsv4_hca_generation_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 211,005 | | `fused_kda_decode` | vllm | kda_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, rtx_pro_6000_server | 308 | | `fused_kda_decode_mtp_dspark` | sglang | kda_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 428 | | `fused_recurrent_gated_delta_rule` | sglang, trtllm | gdn_perf.parquet | a100_sxm, b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 774 | -| `fused_recurrent_gated_delta_rule_packed_decode` | sglang, vllm | gdn_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 6,480 | +| `fused_recurrent_gated_delta_rule_packed_decode` | sglang, vllm | gdn_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 9,638 | | `fused_recurrent_kda` | vllm | kda_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 972 | | `fused_recurrent_kda_packed_decode` | sglang, vllm | kda_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 651 | | `fused_sigmoid_gating_delta_rule_update` | sglang | kda_perf.parquet | h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 432 | | `kda_fused_decode` | sglang | kda_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, rtx_pro_6000_server | 77 | -| `MiniMaxM3SparseMSAImpl` | vllm | msa_context_module_perf.parquet, msa_generation_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 82,176 | -| `MiniMaxM3SparseTritonImpl` | vllm | msa_context_module_perf.parquet, msa_generation_module_perf.parquet | h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 54,716 | -| `moe_torch_flow` | trtllm | moe_perf.parquet | a100_sxm, b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s | 62,657 | +| `MiniMaxM3SparseMSAImpl` | vllm | msa_context_module_perf.parquet, msa_generation_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 138,705 | +| `MiniMaxM3SparseTritonImpl` | vllm | msa_context_module_perf.parquet, msa_generation_module_perf.parquet | h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 77,254 | +| `mla_trtllm_mla` | sglang | mla_context_module_perf.parquet | b200_sxm, b300_sxm | 436 | +| `moe_torch_flow` | trtllm | moe_perf.parquet | a100_sxm, b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s | 62,819 | | `moe_torch_flow_cutlass` | trtllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 1,014,571 | | `moe_torch_flow_min_latency` | trtllm | moe_perf.parquet | b200_sxm, gb200 | 72,090 | | `moe_torch_flow_nongated` | trtllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 194,352 | @@ -1386,9 +1447,16 @@ Each row is one distinct `kernel_source` value seen in the corpus, with the unio | `NVLinkOneSided` | trtllm | trtllm_alltoall_perf.parquet | gb200 | 296 | | `NVLinkTwoSided` | trtllm | trtllm_alltoall_perf.parquet | gb200 | 1,800 | | `sglang` | sglang | computescale_perf.parquet, gemm_perf.parquet, scale_matrix_perf.parquet | a100_sxm, b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 84,087 | -| `SGLang_CustomAllReduce_eager` | sglang | custom_allreduce_perf.parquet | a100_sxm, b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 1,679 | -| `SGLang_CustomAllReduce_graph` | sglang | custom_allreduce_perf.parquet | a100_sxm, b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 1,679 | -| `sglang_deepgemm_gemm_nt_f8f8bf16` | sglang | gemm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 177,156 | +| `sglang.srt.layers.engram.Engram.apply_gate+sglang.srt.layers.engram.EngramEmbedding._owned_rows+sglang.srt.layers.linear.ReplicatedLinear.forward/sglang.srt.layers.quantization.fp8.Fp8LinearMethod` | sglang | dsv41_module_perf.parquet | b200_sxm, gb200, h100_sxm, h200_sxm | 240 | +| `sglang.srt.layers.linear.MergedColumnParallelLinear.forward/sglang.srt.layers.quantization.fp8.Fp8LinearMethod` | sglang | dsv41_module_perf.parquet | b200_sxm, gb200, h100_sxm, h200_sxm | 120 | +| `sglang.srt.layers.linear.RowParallelLinear.forward/sglang.srt.layers.quantization.fp8.Fp8LinearMethod` | sglang | dsv41_module_perf.parquet | b200_sxm, gb200, h100_sxm, h200_sxm | 120 | +| `sglang.srt.layers.vocab_parallel_embedding.ParallelLMHead.quant_method.apply/sglang.srt.layers.quantization.unquant.UnquantizedEmbeddingMethod` | sglang | gemm_perf.parquet | b200_sxm, gb200, h100_sxm, h200_sxm | 120 | +| `sglang.srt.models.deepseek_v2.MoEGate.forward` | sglang | gemm_perf.parquet | b200_sxm, gb200, h100_sxm, h200_sxm | 60 | +| `sglang.srt.models.deepseek_v4.DeepseekV4DecoderLayer._hc_mix_and_combine+sglang.srt.models.deepseek_v4.DeepseekV4DecoderLayer.hc_post` | sglang | dsv41_module_perf.parquet | b200_sxm, gb200, h100_sxm, h200_sxm | 60 | +| `sglang.srt.models.deepseek_v4.MQALayer.forward` | sglang | dsv41_module_perf.parquet | b200_sxm, gb200, h100_sxm, h200_sxm | 9,040 | +| `SGLang_CustomAllReduce_eager` | sglang | custom_allreduce_perf.parquet | a100_sxm, b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 1,725 | +| `SGLang_CustomAllReduce_graph` | sglang | custom_allreduce_perf.parquet | a100_sxm, b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 1,725 | +| `sglang_deepgemm_gemm_nt_f8f8bf16` | sglang | gemm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 295,260 | | `sglang_dsa_dense_mha_fa3` | sglang | dsa_context_module_perf.parquet | h100_sxm, h200_sxm | 82,901 | | `sglang_dsa_dense_mha_trtllm_ragged` | sglang | dsa_context_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 137,411 | | `sglang_dsa_indexer_fa3` | sglang | dsa_generation_module_perf.parquet | h100_sxm, h200_sxm | 5,352 | @@ -1399,30 +1467,31 @@ Each row is one distinct `kernel_source` value seen in the corpus, with the unio | `sglang_dsa_skip_indexer_flashmla_kv` | sglang | dsa_context_module_perf.parquet, dsa_generation_module_perf.parquet | h100_sxm, h200_sxm | 24,610 | | `sglang_dsa_skip_indexer_flashmla_sparse` | sglang | dsa_context_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 54,618 | | `sglang_dsa_skip_indexer_trtllm` | sglang | dsa_context_module_perf.parquet, dsa_generation_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 39,806 | -| `sglang_flashinfer_cutedsl_nvfp4` | sglang | gemm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 118,104 | -| `sglang_flashinfer_cutlass_moe` | sglang | moe_perf.parquet | h100_sxm, h200_sxm, rtx_pro_6000_server | 52,731 | +| `sglang_flashinfer_cutedsl_nvfp4` | sglang | gemm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 236,208 | +| `sglang_flashinfer_cutlass_moe` | sglang | moe_perf.parquet | h100_sxm, h200_sxm, rtx_pro_6000_server | 52,761 | | `sglang_flashinfer_cutlass_nvfp4` | sglang | gemm_perf.parquet | rtx_pro_6000_server | 29,526 | -| `sglang_flashinfer_trtllm_moe` | sglang | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 623,191 | -| `sglang_fused_moe_triton` | sglang | moe_perf.parquet | a100_sxm, b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 659,010 | +| `sglang_flashinfer_trtllm_moe` | sglang | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 645,304 | +| `sglang_fused_moe_triton` | sglang | moe_perf.parquet | a100_sxm, b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 682,986 | | `sglang_fused_moe_triton_situ_as_silu` | sglang | moe_perf.parquet | l40s | 2,997 | | `sglang_marlin_moe` | sglang | moe_perf.parquet | a100_sxm, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 70,795 | | `sglang_marlin_moe_situ_as_silu` | sglang | moe_perf.parquet | h100_sxm, h200_sxm, rtx_pro_6000_server | 1,580 | | `sglang_minimax_decode_triton_sparse_topk_radix` | sglang | msa_generation_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 964 | | `sglang_minimax_decode_triton_sparse_topk_split` | sglang | msa_generation_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 7,894 | | `sglang_minimax_prefill_triton_sparse` | sglang | msa_context_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 160,826 | -| `sglang_mxfp4_flashinfer_trtllm_moe` | sglang | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 29,970 | +| `sglang_mxfp4_flashinfer_trtllm_moe` | sglang | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 30,030 | +| `sglang_mxfp4_humming_moe` | sglang | moe_perf.parquet | h100_sxm, h200_sxm | 30 | | `sglang_sgl_kernel_bmm_fp8` | sglang | mla_bmm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 5,724 | -| `sglang_sgl_kernel_fp8_scaled_mm` | sglang | gemm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 286,084 | +| `sglang_sgl_kernel_fp8_scaled_mm` | sglang | gemm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 429,348 | | `sglang_tilelang_mhc_post` | sglang | mhc_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, rtx_pro_6000_server | 485 | | `sglang_tilelang_mhc_pre` | sglang | mhc_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 420 | | `sglang_torch_bmm` | sglang | mla_bmm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 5,724 | -| `sglang_torch_linear` | sglang | gemm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 286,158 | +| `sglang_torch_linear` | sglang | gemm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 429,718 | | `sglang_torch_mhc_pre` | sglang | mhc_module_perf.parquet | rtx_pro_6000_server | 67 | | `static_scaled_fp8_quant` | vllm | scale_matrix_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 13,024 | | `topk_transform_v1` | sglang | dsv4_csa_topk_calib_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 25,764 | | `topk_transform_v2` | sglang | dsv4_csa_topk_calib_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 1,464 | -| `torch.nn.functional.linear` | vllm | gemm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 285,866 | -| `torch_flow` | trtllm | context_attention_perf.parquet, encoder_attention_perf.parquet, gemm_perf.parquet, generation_attention_perf.parquet | a100_sxm, b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 1,880,467 | +| `torch.nn.functional.linear` | vllm | gemm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 654,534 | +| `torch_flow` | trtllm | context_attention_perf.parquet, encoder_attention_perf.parquet, gemm_perf.parquet, generation_attention_perf.parquet | a100_sxm, b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 1,891,515 | | `torch_flow_flashinfer` | trtllm | context_attention_perf.parquet, generation_attention_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 23,408 | | `torch_ops` | trtllm | computescale_perf.parquet, scale_matrix_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 26,012 | | `triton` | sglang | context_attention_perf.parquet, context_mla_perf.parquet, encoder_attention_perf.parquet, generation_attention_perf.parquet, generation_mla_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 173,342 | @@ -1434,59 +1503,61 @@ Each row is one distinct `kernel_source` value seen in the corpus, with the unio | `trtllm_bmm_out` | trtllm | mla_bmm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 3,392 | | `trtllm_bmm_out_dequant_bf16` | trtllm | mla_bmm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 1,696 | | `trtllm_fp8_block_scaling_bmm_out` | trtllm | mla_bmm_perf.parquet | h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 1,696 | -| `trtllm_mha` | sglang | context_attention_perf.parquet, generation_attention_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 266,910 | +| `trtllm_mha` | sglang | context_attention_perf.parquet, generation_attention_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 273,765 | | `trtllm_mhc_post_mapping` | trtllm | mhc_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 555 | | `trtllm_mhc_pre_dg_nosplit` | trtllm | mhc_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 159 | | `trtllm_mhc_pre_dg_splitk` | trtllm | mhc_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 156 | | `trtllm_mhc_pre_fma` | trtllm | mhc_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 231 | -| `trtllm_mla` | sglang | context_mla_perf.parquet, generation_mla_perf.parquet, wideep_context_mla_perf.parquet, wideep_generation_mla_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 38,503 | +| `trtllm_mla` | sglang | context_mla_perf.parquet, generation_mla_perf.parquet, wideep_context_mla_perf.parquet, wideep_generation_mla_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 39,383 | | `TRTLLM_MNNVL_oneshot` | trtllm | custom_allreduce_perf.parquet | gb300 | 19 | | `TRTLLM_MNNVL_twoshot` | trtllm | custom_allreduce_perf.parquet | gb300 | 27 | -| `TRTLLM_RAGGED` | vllm | mla_context_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 17,591 | -| `vllm.model_executor.kernels.mhc.tilelang.mhc_post_tilelang` | vllm | mhc_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s | 486 | -| `vllm.model_executor.kernels.mhc.tilelang.mhc_pre_tilelang` | vllm | mhc_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s | 476 | -| `vllm.utils.deep_gemm.fp8_fp4_paged_mqa_logits` | vllm | dsv4_paged_mqa_logits_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 11,754 | +| `TRTLLM_RAGGED` | vllm | mla_context_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 33,863 | +| `vllm.model_executor.kernels.mhc.tilelang.mhc_post_tilelang` | vllm | mhc_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s | 905 | +| `vllm.model_executor.kernels.mhc.tilelang.mhc_pre_tilelang` | vllm | mhc_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s | 896 | +| `vllm.utils.deep_gemm.fp8_fp4_paged_mqa_logits` | vllm | dsv4_paged_mqa_logits_module_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 35,262 | | `vllm_compressedtensorsw4a4mxfp4moe_marlin_marlinexperts` | vllm | moe_perf.parquet | b200_sxm, b300_sxm | 1,944 | -| `vllm_compressedtensorsw4a4mxfp4moe_marlin_marlinexperts_situ_as_silu` | vllm | moe_perf.parquet | gb200, gb300, h100_sxm, h200_sxm | 3,888 | -| `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_cutlass_flashinferexperts` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, rtx_pro_6000_server | 19,710 | -| `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_trtllm_trtllmnvfp4expertsmodular` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 6,804 | -| `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_trtllm_trtllmnvfp4expertsmonolithic` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 58,116 | -| `vllm_compressedtensorswna16marlinmoe_marlin_marlinexperts` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 8,397 | +| `vllm_compressedtensorsw4a4mxfp4moe_marlin_marlinexperts_situ_as_silu` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 9,720 | +| `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_cutlass_flashinferexperts` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, rtx_pro_6000_server | 23,598 | +| `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_trtllm_trtllmnvfp4expertsmodular` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 20,412 | +| `vllm_compressedtensorsw4a4nvfp4moe_flashinfer_trtllm_trtllmnvfp4expertsmonolithic` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 117,220 | +| `vllm_compressedtensorswna16marlinmoe_marlin_marlinexperts` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 14,715 | | `vLLM_custom_eager` | vllm | custom_allreduce_perf.parquet | a100_sxm, b200_sxm, b300_sxm, b60, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 1,633 | | `vLLM_custom_graph` | vllm | custom_allreduce_perf.parquet | a100_sxm, b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 1,426 | | `vllm_default` | vllm | gemm_perf.parquet | a100_sxm, b60 | 49,770 | | `vllm_flash_attn` | vllm | context_attention_perf.parquet, generation_attention_perf.parquet | a100_sxm, b60 | 102,215 | | `vllm_flash_attn_fa2` | vllm | context_attention_perf.parquet, generation_attention_perf.parquet | l40s, rtx_pro_6000_server | 111,574 | -| `vllm_flash_attn_fa3` | vllm | context_attention_perf.parquet, generation_attention_perf.parquet | h100_sxm, h200_sxm | 223,236 | -| `vllm_flash_attn_fa4` | vllm | context_attention_perf.parquet, generation_attention_perf.parquet | h100_sxm, h200_sxm | 2,756 | +| `vllm_flash_attn_fa3` | vllm | context_attention_perf.parquet, generation_attention_perf.parquet | h100_sxm, h200_sxm | 463,230 | +| `vllm_flash_attn_fa4` | vllm | context_attention_perf.parquet, generation_attention_perf.parquet | h100_sxm, h200_sxm | 6,006 | | `vllm_flashinfer` | vllm | context_attention_perf.parquet, generation_attention_perf.parquet | b200_sxm | 71,600 | | `vllm_flashinfer_fidecode` | vllm | context_attention_perf.parquet, generation_attention_perf.parquet | l40s, rtx_pro_6000_server | 58,484 | | `vllm_flashinfer_fiprefill` | vllm | context_attention_perf.parquet | l40s, rtx_pro_6000_server | 43,367 | +| `vllm_flashinfer_flashinfertrtllmapidecode` | vllm | context_attention_perf.parquet, generation_attention_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 268,320 | | `vllm_flashinfer_trtllm_moe_fp4` | vllm | moe_perf.parquet | b200_sxm | 10,287 | -| `vllm_flashinfer_trtllmdecode` | vllm | context_attention_perf.parquet, generation_attention_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 249,800 | -| `vllm_flashinfer_trtllmprefill` | vllm | context_attention_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 178,092 | -| `vllm_fp8moe_flashinfer_cutlass_flashinferexperts` | vllm | moe_perf.parquet | h100_sxm, h200_sxm | 18,468 | -| `vllm_fp8moe_flashinfer_trtllm_trtllmfp8expertsmodular` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 15,228 | -| `vllm_fp8moe_flashinfer_trtllm_trtllmfp8expertsmonolithic` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 45,684 | -| `vllm_fp8moe_triton_tritonexperts` | vllm | moe_perf.parquet | h100_sxm, h200_sxm, l40s | 27,100 | +| `vllm_flashinfer_trtllmdecode` | vllm | context_attention_perf.parquet, generation_attention_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 254,018 | +| `vllm_flashinfer_trtllmprefill` | vllm | context_attention_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 369,948 | +| `vllm_fp8moe_flashinfer_cutlass_flashinferexperts` | vllm | moe_perf.parquet | h100_sxm, h200_sxm | 39,042 | +| `vllm_fp8moe_flashinfer_trtllm_trtllmfp8expertsmodular` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 32,724 | +| `vllm_fp8moe_flashinfer_trtllm_trtllmfp8expertsmonolithic` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 99,792 | +| `vllm_fp8moe_triton_tritonexperts` | vllm | moe_perf.parquet | h100_sxm, h200_sxm, l40s | 40,222 | | `vllm_fused_moe` | vllm | moe_perf.parquet | a100_sxm, b200_sxm, h200_sxm | 126,055 | -| `vllm_gptossmxfp4moe_flashinfer_trtllm_mxfp4_bf16_trtllmmxfp4expertsmonolithic` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 7,776 | +| `vllm_gptossmxfp4moe_flashinfer_trtllm_mxfp4_bf16_trtllmmxfp4expertsmonolithic` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 15,552 | | `vllm_gptossmxfp4moe_marlin_marlinexperts` | vllm | moe_perf.parquet | l40s, rtx_pro_6000_server | 3,888 | -| `vllm_gptossmxfp4moe_triton_oaitritonmxfp4expertsmonolithic` | vllm | moe_perf.parquet | h100_sxm, h200_sxm | 3,888 | +| `vllm_gptossmxfp4moe_triton_oaitritonmxfp4expertsmonolithic` | vllm | moe_perf.parquet | h100_sxm, h200_sxm | 7,776 | | `vllm_marlin_int4_moe` | vllm | moe_perf.parquet | b200_sxm, h200_sxm | 20,415 | -| `vllm_modeloptfp8moe_flashinfer_cutlass_flashinferexperts` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, rtx_pro_6000_server | 46,008 | -| `vllm_modeloptfp8moe_flashinfer_trtllm_trtllmfp8expertsmonolithic` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 46,003 | +| `vllm_modeloptfp8moe_flashinfer_cutlass_flashinferexperts` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, rtx_pro_6000_server | 84,078 | +| `vllm_modeloptfp8moe_flashinfer_trtllm_trtllmfp8expertsmonolithic` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 95,937 | | `vllm_modeloptfp8moe_triton_tritonexperts` | vllm | moe_perf.parquet | l40s | 14,174 | | `vllm_mxfp4_moe` | vllm | moe_perf.parquet | b200_sxm, h200_sxm | 2,839 | -| `vllm_mxfp4moe_flashinfer_trtllm_mxfp4_mxfp8_trtllmmxfp4expertsmodular` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 8,748 | -| `vllm_torch_bmm` | vllm | mla_bmm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 4,252 | -| `vllm_triton_attn` | vllm | context_attention_perf.parquet, generation_attention_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, l40s, rtx_pro_6000_server | 27,020 | -| `vllm_unquantizedfusedmoe_flashinfer_cutlass_flashinferexperts` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, rtx_pro_6000_server | 23,679 | -| `vllm_unquantizedfusedmoe_flashinfer_trtllm_trtllmbf16experts` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 49,235 | -| `vllm_unquantizedfusedmoe_triton_tritonexperts` | vllm | moe_perf.parquet | h100_sxm, h200_sxm, l40s | 48,589 | +| `vllm_mxfp4moe_flashinfer_trtllm_mxfp4_mxfp8_trtllmmxfp4expertsmodular` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 17,496 | +| `vllm_torch_bmm` | vllm | mla_bmm_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm | 8,068 | +| `vllm_triton_attn` | vllm | context_attention_perf.parquet, generation_attention_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, l40s, rtx_pro_6000_server | 42,460 | +| `vllm_unquantizedfusedmoe_flashinfer_cutlass_flashinferexperts` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, rtx_pro_6000_server | 30,807 | +| `vllm_unquantizedfusedmoe_flashinfer_trtllm_trtllmbf16experts` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 103,173 | +| `vllm_unquantizedfusedmoe_flashinfer_trtllm_trtllmbf16expertsmonolithic` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 4,158 | +| `vllm_unquantizedfusedmoe_triton_tritonexperts` | vllm | moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s | 89,413 | | `vllm_vit_flash_attn_fa2` | vllm | encoder_attention_perf.parquet | l40s, rtx_pro_6000_server | 15,358 | -| `vllm_vit_flash_attn_fa3` | vllm | encoder_attention_perf.parquet | h100_sxm, h200_sxm | 15,358 | -| `vllm_vit_flash_attn_fa4` | vllm | encoder_attention_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 30,716 | +| `vllm_vit_flash_attn_fa3` | vllm | encoder_attention_perf.parquet | h100_sxm, h200_sxm | 30,716 | +| `vllm_vit_flash_attn_fa4` | vllm | encoder_attention_perf.parquet | b200_sxm, b300_sxm, gb200, gb300 | 61,432 | | `vllm_xpu_moe` | vllm | moe_perf.parquet | b60 | 1,836 | | `vllm_xpu_moe_mxfp4` | vllm | moe_perf.parquet | b60 | 2,592 | | `wideep_compute_cutlass` | trtllm | wideep_moe_perf.parquet | b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, rtx_pro_6000_server | 26,298 | @@ -1495,4 +1566,4 @@ Each row is one distinct `kernel_source` value seen in the corpus, with the unio | kernel_source | frameworks | op files | systems | rows | |---|---|---|---|---| -| `default` | sglang, trtllm | context_mla_perf.parquet, dsa_context_module_perf.parquet, dsa_generation_module_perf.parquet, generation_mla_perf.parquet, mla_bmm_perf.parquet, mla_context_module_perf.parquet, mla_generation_module_perf.parquet, msa_context_module_perf.parquet, msa_generation_module_perf.parquet | a100_sxm, b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 303,641 | +| `default` | sglang, trtllm | context_mla_perf.parquet, dsa_context_module_perf.parquet, dsa_generation_module_perf.parquet, generation_mla_perf.parquet, mla_bmm_perf.parquet, mla_context_module_perf.parquet, mla_generation_module_perf.parquet, msa_context_module_perf.parquet, msa_generation_module_perf.parquet | a100_sxm, b200_sxm, b300_sxm, gb200, gb300, h100_sxm, h200_sxm, l40s, rtx_pro_6000_server | 306,281 |