1use std::sync::Arc;
20
21use crate::common::enums::{DatabaseMode, TransferPolicy};
22use crate::common::error::AicError;
23use crate::engine::spec::EngineSpec;
24use crate::operators::base::PerformanceResult;
25use crate::operators::{FpmForwardOp, FpmPhase, Op};
26use crate::perf_database::PerfDatabase;
27use crate::session::{
28 get_mix_step_ops, query_context_op, query_generation_op, run_context_ops, run_context_ops_with,
29 run_generation_ops_step, run_generation_ops_step_beamed_with, ContextOpFilter,
30};
31use crate::{validate_forward_pass_metrics, ForwardPassMetrics};
32
33#[derive(Clone, Copy, Debug, PartialEq)]
42pub struct RuntimeConfig {
43 pub batch_size: u32,
44 pub beam_width: u32,
48 pub isl: u32,
49 pub osl: u32,
50 pub prefix: u32,
52 pub seq_imbalance_correction_scale: f64,
54 pub gen_seq_imbalance_correction_scale: f64,
56}
57
58impl Default for RuntimeConfig {
59 fn default() -> Self {
60 Self {
61 batch_size: 1,
62 beam_width: 1,
63 isl: 1,
64 osl: 1,
65 prefix: 0,
66 seq_imbalance_correction_scale: 1.0,
67 gen_seq_imbalance_correction_scale: 1.0,
68 }
69 }
70}
71
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub enum StaticMode {
76 Context,
78 Generation,
80 Both,
82}
83
84#[derive(Clone, Debug, PartialEq)]
90pub struct StaticResult {
91 pub context_ms: f64,
93 pub generation_ms: f64,
95 pub total_ms: f64,
97}
98
99pub const DEFAULT_STATIC_STRIDE: u32 = 32;
102
103pub(crate) type MoeCommFallbackValue = (&'static str, &'static str, u32, u32, u32, u32);
107
108pub(crate) type MoeCommFallbackValues = (MoeCommFallbackValue, Vec<MoeCommFallbackValue>);
112
113pub type PerOpValue = (String, f64, f64, &'static str);
123
124pub(crate) type PerOpValueWithMetadata = (
129 String,
130 f64,
131 f64,
132 &'static str,
133 Option<MoeCommFallbackValues>,
134);
135
136pub(crate) type MixedStepPerOpValuesWithMetadata = (
139 Vec<PerOpValueWithMetadata>,
140 Vec<PerOpValueWithMetadata>,
141 Vec<PerOpValueWithMetadata>,
142);
143
144pub type PerOpSolValue = (String, f64, f64, f64);
152
153struct PerOpFold {
158 inference_phase: &'static str,
159 entries: Vec<PerOpValueWithMetadata>,
160}
161
162fn insert_per_op_fallback(
163 fallbacks: &mut Option<MoeCommFallbackValues>,
164 fallback: MoeCommFallbackValue,
165) {
166 match fallbacks {
167 None => *fallbacks = Some((fallback, Vec::new())),
168 Some((first, additional)) if *first == fallback || additional.contains(&fallback) => {}
169 Some((_first, additional)) => additional.push(fallback),
170 }
171}
172
173fn extend_per_op_fallbacks(
174 fallbacks: &mut Option<MoeCommFallbackValues>,
175 other: Option<MoeCommFallbackValues>,
176) {
177 let Some((first, additional)) = other else {
178 return;
179 };
180 insert_per_op_fallback(fallbacks, first);
181 for fallback in additional {
182 insert_per_op_fallback(fallbacks, fallback);
183 }
184}
185
186impl PerOpFold {
187 fn new(inference_phase: &'static str) -> Self {
188 Self {
189 inference_phase,
190 entries: Vec::new(),
191 }
192 }
193
194 fn add(&mut self, op: &Op, r: PerformanceResult) {
195 let name = op.name();
196 let source = r.source.as_str();
197 let mut fallbacks = None;
198 for fallback in r.moe_comm_fallbacks.iter() {
199 insert_per_op_fallback(
200 &mut fallbacks,
201 (
202 self.inference_phase,
203 fallback.comm_backend,
204 fallback.requested_ep_size,
205 fallback.requested_node_num,
206 fallback.measurement_ep_size,
207 fallback.measurement_node_num,
208 ),
209 );
210 }
211 if let Some(entry) = self.entries.iter_mut().find(|e| e.0 == name) {
212 entry.1 += r.latency_ms;
213 entry.2 += r.energy_wms;
214 if entry.3 != source {
215 entry.3 = "mixed";
216 }
217 extend_per_op_fallbacks(&mut entry.4, fallbacks);
218 return;
219 }
220 self.entries.push((
221 name.to_string(),
222 r.latency_ms,
223 r.energy_wms,
224 source,
225 fallbacks,
226 ));
227 }
228
229 fn into_values(self) -> Vec<PerOpValueWithMetadata> {
230 self.entries
231 }
232}
233
234fn strip_per_op_metadata(entries: Vec<PerOpValueWithMetadata>) -> Vec<PerOpValue> {
235 entries
236 .into_iter()
237 .map(|(name, latency_ms, energy_wms, source, _fallbacks)| {
238 (name, latency_ms, energy_wms, source)
239 })
240 .collect()
241}
242
243#[derive(Default)]
246struct PerOpSolFold {
247 entries: Vec<PerOpSolValue>,
248}
249
250impl PerOpSolFold {
251 fn add(&mut self, op: &Op, r: PerformanceResult) -> Result<(), AicError> {
252 let (sol_math, sol_mem) = match r.sol {
253 Some(c) => (c.math_ms, c.mem_ms),
254 None if r.latency_ms == 0.0 && r.energy_wms == 0.0 => (0.0, 0.0),
258 None => {
259 return Err(AicError::SolNotImplemented(format!(
260 "evaluate_ops_sol_json: op '{}' has no SOL decomposition \
261 (family not exported yet — see PerformanceResult::sol)",
262 op.name()
263 )))
264 }
265 };
266 if let Some(entry) = self.entries.iter_mut().find(|e| e.0 == op.name()) {
267 entry.1 += r.latency_ms;
268 entry.2 += sol_math;
269 entry.3 += sol_mem;
270 return Ok(());
271 }
272 self.entries
273 .push((op.name().to_string(), r.latency_ms, sol_math, sol_mem));
274 Ok(())
275 }
276
277 fn into_values(self) -> Vec<PerOpSolValue> {
278 self.entries
279 }
280}
281
282#[derive(Clone, Copy, Debug, PartialEq, Eq)]
284enum MixedPass {
285 SharedNonAttention,
286 ContextAttention,
287 DecodeAttention,
288}
289
290pub struct Engine {
298 context_ops: Vec<Op>,
300 generation_ops: Vec<Op>,
302 db: Arc<PerfDatabase>,
305 nextn: u32,
309}
310
311impl std::fmt::Debug for Engine {
312 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313 f.debug_struct("Engine")
314 .field("context_ops", &self.context_ops.len())
315 .field("generation_ops", &self.generation_ops.len())
316 .field("nextn", &self.nextn)
317 .finish_non_exhaustive()
318 }
319}
320
321impl Engine {
322 pub fn build(spec: EngineSpec, db: Arc<PerfDatabase>) -> Result<Engine, AicError> {
328 let nextn = spec
329 .engine
330 .speculative
331 .as_ref()
332 .and_then(|s| s.nextn)
333 .unwrap_or(0);
334 fn contains_fpm(ops: &[Op]) -> bool {
345 ops.iter().any(|op| match op {
346 Op::FpmForward(_) => true,
347 Op::Overlap(o) => contains_fpm(&o.group_a) || contains_fpm(&o.group_b),
348 Op::Fallback(o) => {
349 contains_fpm(std::slice::from_ref(&o.primary)) || contains_fpm(&o.fallback)
350 }
351 _ => false,
352 })
353 }
354 let any_fpm = contains_fpm(&spec.context_ops) || contains_fpm(&spec.generation_ops);
355 if any_fpm {
356 let shape_ok = matches!(
357 spec.context_ops.as_slice(),
358 [Op::FpmForward(p)] if p.phase == FpmPhase::Prefill
359 ) && matches!(
360 spec.generation_ops.as_slice(),
361 [Op::FpmForward(d)] if d.phase == FpmPhase::Decode
362 );
363 if !shape_ok {
364 return Err(AicError::InvalidEngineConfig(
365 "forward_model='fpm' spec must contain exactly one FpmForward op per phase \
366 (prefill in context_ops, decode in generation_ops)"
367 .to_string(),
368 ));
369 }
370 if nextn > 0 {
371 return Err(AicError::InvalidEngineConfig(format!(
372 "forward_model='fpm' does not support MTP speculative decoding (nextn={nextn})"
373 )));
374 }
375 }
376 Ok(Engine {
377 context_ops: spec.context_ops,
378 generation_ops: spec.generation_ops,
379 db,
380 nextn,
381 })
382 }
383
384 fn fpm_ops(&self) -> Option<(&FpmForwardOp, &FpmForwardOp)> {
387 match (self.context_ops.as_slice(), self.generation_ops.as_slice()) {
388 ([Op::FpmForward(p)], [Op::FpmForward(d)]) => Some((p, d)),
389 _ => None,
390 }
391 }
392
393 pub fn from_spec_bytes(
402 bytes: &[u8],
403 systems_root: &std::path::Path,
404 ) -> Result<Engine, AicError> {
405 let spec = EngineSpec::from_bincode(bytes)?;
406 let version = spec.engine.backend_version.as_deref().ok_or_else(|| {
407 AicError::InvalidEngineConfig(
408 "backend_version is required to load the perf database".to_string(),
409 )
410 })?;
411 let systems_root = spec.engine.systems_path.as_deref().unwrap_or(systems_root);
414 let transfer_policy = TransferPolicy::from_wire(spec.engine.transfer_policy.as_deref())
415 .map_err(AicError::InvalidEngineConfig)?;
416 let db = PerfDatabase::load_resolved_shared(
423 systems_root,
424 &spec.engine.system_name,
425 spec.engine.backend.as_str(),
426 version,
427 spec.engine.enable_shared_layer.unwrap_or(matches!(
432 spec.engine.database_mode,
433 DatabaseMode::Silicon | DatabaseMode::Hybrid
434 )),
435 spec.engine.strict_provenance,
436 spec.engine.database_mode == DatabaseMode::Sol || spec.engine.tolerate_dirless_version,
445 )?
446 .with_mode(spec.engine.database_mode, transfer_policy);
447 Engine::build(spec, Arc::new(db))
448 }
449
450 pub fn database(&self) -> &Arc<PerfDatabase> {
452 &self.db
453 }
454
455 pub fn reset_provenance(&self) {
462 self.db.reset_provenance();
463 }
464
465 pub fn last_provenance(&self) -> Option<&'static str> {
470 match self.db.worst_provenance() {
471 crate::operators::util_empirical::ProvenanceTier::Silicon => None,
472 tier => Some(tier.as_str()),
473 }
474 }
475
476 #[cfg(test)]
480 pub(crate) fn context_ops_for_test(&self) -> &[Op] {
481 &self.context_ops
482 }
483
484 #[cfg(test)]
487 pub(crate) fn generation_ops_for_test(&self) -> &[Op] {
488 &self.generation_ops
489 }
490
491 pub fn run_static(
495 &self,
496 runtime: &RuntimeConfig,
497 mode: StaticMode,
498 stride: u32,
499 ) -> Result<StaticResult, AicError> {
500 let context_ms = match mode {
501 StaticMode::Context | StaticMode::Both => self.run_context_phase(runtime)?,
502 StaticMode::Generation => 0.0,
503 };
504 let generation_ms = match mode {
505 StaticMode::Generation | StaticMode::Both => {
506 self.run_generation_phase(runtime, stride)?
507 }
508 StaticMode::Context => 0.0,
509 };
510 Ok(StaticResult {
511 context_ms,
512 generation_ms,
513 total_ms: context_ms + generation_ms,
514 })
515 }
516
517 fn run_context_phase(&self, runtime: &RuntimeConfig) -> Result<f64, AicError> {
520 if runtime.prefix >= runtime.isl {
522 return Err(AicError::InvalidEngineConfig(format!(
523 "isl must be greater than 0 after removing prefix, but got {}",
524 runtime.isl as i64 - runtime.prefix as i64
525 )));
526 }
527 let effective_isl = runtime.isl - runtime.prefix;
528 run_context_ops(
529 &self.context_ops,
530 &self.db,
531 runtime.batch_size,
532 effective_isl,
533 runtime.prefix,
534 runtime.seq_imbalance_correction_scale,
535 ContextOpFilter::All,
536 )
537 }
538
539 fn run_generation_phase(&self, runtime: &RuntimeConfig, stride: u32) -> Result<f64, AicError> {
553 self.run_generation_phase_with(runtime, stride, |_, _| {})
554 }
555
556 fn run_generation_phase_with(
564 &self,
565 runtime: &RuntimeConfig,
566 stride: u32,
567 mut on_op: impl FnMut(&Op, PerformanceResult),
568 ) -> Result<f64, AicError> {
569 let bs = runtime
570 .batch_size
571 .saturating_mul(self.nextn.saturating_add(1));
572 let stride = stride.max(1);
573 let mut total = 0.0_f64;
574 if runtime.osl <= 1 {
575 return Ok(0.0);
576 }
577 let upper = runtime.osl - 1; let mut i = 0u32;
579 while i < upper {
580 let s = runtime.isl + i + 1;
583 let repeat_count = stride.min(upper - i);
584 let mut step_fold: Vec<(&Op, PerformanceResult)> = Vec::new();
588 let step = run_generation_ops_step_beamed_with(
589 &self.generation_ops,
590 &self.db,
591 bs,
592 runtime.beam_width,
593 s,
594 runtime.gen_seq_imbalance_correction_scale,
595 false,
596 |op, r| {
597 if let Some(entry) = step_fold.iter_mut().find(|(e, _)| e.name() == op.name()) {
598 entry.1.latency_ms += r.latency_ms;
599 entry.1.energy_wms += r.energy_wms;
600 if entry.1.source != r.source {
601 entry.1.source = crate::operators::base::Source::Mixed;
602 }
603 entry.1.moe_comm_fallbacks.extend(r.moe_comm_fallbacks);
604 } else {
605 step_fold.push((op, r));
606 }
607 },
608 )?;
609 for (op, folded) in step_fold {
610 on_op(op, folded.scaled(repeat_count as f64));
611 }
612 total += step * repeat_count as f64;
613 i += stride;
614 }
615 Ok(total)
616 }
617
618 pub fn predict_prefill_latency(&self, bs: u32, isl: u32, prefix: u32) -> Result<f64, AicError> {
623 let rt = RuntimeConfig {
624 batch_size: bs,
625 isl,
626 osl: 1,
627 prefix,
628 ..Default::default()
629 };
630 Ok(self
631 .run_static(&rt, StaticMode::Context, DEFAULT_STATIC_STRIDE)?
632 .total_ms)
633 }
634
635 pub fn predict_decode_latency(&self, bs: u32, isl: u32, osl: u32) -> Result<f64, AicError> {
640 let rt = RuntimeConfig {
641 batch_size: bs,
642 isl,
643 osl,
644 ..Default::default()
645 };
646 Ok(self
647 .run_static(&rt, StaticMode::Generation, DEFAULT_STATIC_STRIDE)?
648 .total_ms)
649 }
650
651 pub fn mixed_step_latency(
685 &self,
686 ctx_tokens: u32,
687 gen_tokens: u32,
688 isl: u32,
689 osl: u32,
690 prefix: u32,
691 seq_imbalance_correction_scale: f64,
692 gen_seq_imbalance_correction_scale: f64,
693 ) -> Result<f64, AicError> {
694 Ok(self.mixed_step_breakdown(
695 ctx_tokens,
696 gen_tokens,
697 isl,
698 osl,
699 prefix,
700 seq_imbalance_correction_scale,
701 gen_seq_imbalance_correction_scale,
702 )?[0])
703 }
704
705 pub fn mixed_step_breakdown(
713 &self,
714 ctx_tokens: u32,
715 gen_tokens: u32,
716 isl: u32,
717 osl: u32,
718 prefix: u32,
719 seq_imbalance_correction_scale: f64,
720 gen_seq_imbalance_correction_scale: f64,
721 ) -> Result<[f64; 4], AicError> {
722 self.mixed_step_breakdown_with(
723 ctx_tokens,
724 gen_tokens,
725 isl,
726 osl,
727 prefix,
728 seq_imbalance_correction_scale,
729 gen_seq_imbalance_correction_scale,
730 |_, _, _| {},
731 )
732 }
733
734 #[allow(clippy::too_many_arguments)]
740 fn mixed_step_breakdown_with(
741 &self,
742 ctx_tokens: u32,
743 gen_tokens: u32,
744 isl: u32,
745 osl: u32,
746 prefix: u32,
747 seq_imbalance_correction_scale: f64,
748 gen_seq_imbalance_correction_scale: f64,
749 mut on_op: impl FnMut(MixedPass, &Op, PerformanceResult),
750 ) -> Result<[f64; 4], AicError> {
751 if ctx_tokens == 0 && gen_tokens == 0 {
752 return Ok([0.0; 4]);
753 }
754 if let Some((prefill_op, decode_op)) = self.fpm_ops() {
763 let (prefill_ms, marginal_decode_ms) = self.fpm_mixed_step_components(
764 prefill_op,
765 decode_op,
766 ctx_tokens,
767 gen_tokens,
768 isl.max(1),
769 osl.max(1),
770 prefix,
771 )?;
772 return Ok([
773 prefill_ms + marginal_decode_ms,
774 prefill_ms,
775 0.0,
776 marginal_decode_ms,
777 ]);
778 }
779 let isl = isl.max(1);
783
784 let decode_query_tokens = gen_tokens.saturating_mul(self.nextn.saturating_add(1));
791 let combined = ctx_tokens + decode_query_tokens;
792 let prefix1 = prefix * (ctx_tokens / isl); if prefix1 >= combined {
794 return Err(AicError::InvalidEngineConfig(format!(
795 "isl must be greater than 0 after removing prefix, but got {}",
796 combined as i64 - prefix1 as i64
797 )));
798 }
799 let shared_non_attention = run_context_ops_with(
800 &self.context_ops,
801 &self.db,
802 1,
803 combined - prefix1,
804 prefix1,
805 seq_imbalance_correction_scale,
806 ContextOpFilter::SkipContextAttention,
807 |op, r| on_op(MixedPass::SharedNonAttention, op, r),
808 )?;
809
810 let mut context_attention = 0.0_f64;
815 if ctx_tokens > 0 {
816 if prefix >= isl {
817 return Err(AicError::InvalidEngineConfig(format!(
818 "isl must be greater than 0 after removing prefix, but got {}",
819 isl as i64 - prefix as i64
820 )));
821 }
822 let batch2 = ctx_tokens.div_ceil(isl);
823 let scale2 = isl.div_ceil(ctx_tokens) as f64;
824 let attn = run_context_ops_with(
825 &self.context_ops,
826 &self.db,
827 batch2,
828 isl - prefix,
829 prefix,
830 seq_imbalance_correction_scale,
831 ContextOpFilter::OnlyContextAttention,
832 |op, r| on_op(MixedPass::ContextAttention, op, r),
838 )?;
839 context_attention = attn / scale2;
840 }
841
842 let mut decode_attention = 0.0_f64;
844 if gen_tokens > 0 {
845 let bs = gen_tokens.saturating_mul(self.nextn.saturating_add(1));
846 let s = isl + osl / 2 + 1;
849 decode_attention = run_generation_ops_step_beamed_with(
850 &self.generation_ops,
851 &self.db,
852 bs,
853 1,
854 s,
855 gen_seq_imbalance_correction_scale,
856 true,
857 |op, r| on_op(MixedPass::DecodeAttention, op, r),
858 )?;
859 }
860
861 Ok([
862 shared_non_attention + context_attention + decode_attention,
863 shared_non_attention,
864 context_attention,
865 decode_attention,
866 ])
867 }
868
869 pub fn decode_step_latency(
876 &self,
877 gen_tokens: u32,
878 isl: u32,
879 osl: u32,
880 gen_seq_imbalance_correction_scale: f64,
881 ) -> Result<f64, AicError> {
882 if gen_tokens == 0 {
883 return Ok(0.0);
884 }
885 if self.fpm_ops().is_some() {
891 let rt = RuntimeConfig {
892 batch_size: gen_tokens,
893 isl: isl.saturating_add(osl / 2),
894 osl: 2,
895 ..Default::default()
896 };
897 return self.run_generation_phase(&rt, DEFAULT_STATIC_STRIDE);
898 }
899 let effective_batch = gen_tokens.saturating_mul(self.nextn.saturating_add(1));
900 let s = isl.max(1).saturating_add(osl.max(1) / 2).saturating_add(1);
901 run_generation_ops_step(
902 &self.generation_ops,
903 &self.db,
904 effective_batch,
905 s,
906 gen_seq_imbalance_correction_scale,
907 false,
908 )
909 }
910
911 fn fpm_mixed_step_components(
921 &self,
922 prefill_op: &FpmForwardOp,
923 decode_op: &FpmForwardOp,
924 ctx_tokens: u32,
925 gen_tokens: u32,
926 isl: u32,
927 osl: u32,
928 prefix: u32,
929 ) -> Result<(f64, f64), AicError> {
930 let mut prefill_component = 0.0_f64;
931 if ctx_tokens > 0 {
932 let new_tokens = isl.saturating_sub(prefix);
933 if new_tokens == 0 {
934 return Err(AicError::PerfDatabase(format!(
935 "isl must be greater than prefix, got isl={isl} prefix={prefix}"
936 )));
937 }
938 if ctx_tokens >= new_tokens {
939 let batch = ctx_tokens.div_ceil(new_tokens);
942 prefill_component = prefill_op
943 .query_totals(
944 &self.db,
945 &[
946 batch as f64,
947 (ctx_tokens + gen_tokens) as f64,
948 (batch * prefix) as f64,
949 ],
950 )?
951 .latency_ms;
952 } else {
953 let mut total = 0.0_f64;
955 let mut chunks = 0u32;
956 let mut done = 0u32;
957 while done < new_tokens {
958 let chunk = ctx_tokens.min(new_tokens - done);
959 total += prefill_op
960 .query_totals(
961 &self.db,
962 &[1.0, (chunk + gen_tokens) as f64, (prefix + done) as f64],
963 )?
964 .latency_ms;
965 done += chunk;
966 chunks += 1;
967 }
968 prefill_component = total / chunks as f64;
969 }
970 }
971 let mut marginal_decode = 0.0_f64;
972 if gen_tokens > 0 {
973 let rt = RuntimeConfig {
974 batch_size: gen_tokens,
975 isl: isl.saturating_add(osl / 2),
976 osl: 2,
977 ..Default::default()
978 };
979 let gen_ms = self.run_generation_phase(&rt, DEFAULT_STATIC_STRIDE)?;
980 let baseline_ms = if ctx_tokens > 0 {
981 decode_op
984 .query_pass_baseline(
985 &self.db,
986 gen_tokens.saturating_mul(self.nextn.saturating_add(1)),
987 )?
988 .latency_ms
989 } else {
990 0.0
991 };
992 marginal_decode = (gen_ms - baseline_ms).max(0.0);
993 }
994 Ok((prefill_component, marginal_decode))
995 }
996
997 pub fn run_static_per_op(
1003 &self,
1004 runtime: &RuntimeConfig,
1005 mode: StaticMode,
1006 stride: u32,
1007 ) -> Result<(Vec<PerOpValue>, Vec<PerOpValue>), AicError> {
1008 let (context, generation) = self.run_static_per_op_impl(runtime, mode, stride)?;
1009 Ok((
1010 strip_per_op_metadata(context),
1011 strip_per_op_metadata(generation),
1012 ))
1013 }
1014
1015 pub(crate) fn run_static_per_op_with_metadata(
1019 &self,
1020 runtime: &RuntimeConfig,
1021 mode: StaticMode,
1022 stride: u32,
1023 ) -> Result<(Vec<PerOpValueWithMetadata>, Vec<PerOpValueWithMetadata>), AicError> {
1024 self.run_static_per_op_impl(runtime, mode, stride)
1025 }
1026
1027 fn run_static_per_op_impl(
1028 &self,
1029 runtime: &RuntimeConfig,
1030 mode: StaticMode,
1031 stride: u32,
1032 ) -> Result<(Vec<PerOpValueWithMetadata>, Vec<PerOpValueWithMetadata>), AicError> {
1033 let mut context = PerOpFold::new("context");
1034 if matches!(mode, StaticMode::Context | StaticMode::Both) {
1035 if runtime.prefix >= runtime.isl {
1036 return Err(AicError::InvalidEngineConfig(format!(
1037 "isl must be greater than 0 after removing prefix, but got {}",
1038 runtime.isl as i64 - runtime.prefix as i64
1039 )));
1040 }
1041 run_context_ops_with(
1042 &self.context_ops,
1043 &self.db,
1044 runtime.batch_size,
1045 runtime.isl - runtime.prefix,
1046 runtime.prefix,
1047 runtime.seq_imbalance_correction_scale,
1048 ContextOpFilter::All,
1049 |op, r| context.add(op, r),
1050 )?;
1051 }
1052 let mut generation = PerOpFold::new("generation");
1053 if matches!(mode, StaticMode::Generation | StaticMode::Both) {
1054 self.run_generation_phase_with(runtime, stride, |op, r| generation.add(op, r))?;
1055 }
1056 Ok((context.into_values(), generation.into_values()))
1057 }
1058
1059 #[allow(clippy::too_many_arguments)]
1064 pub fn mixed_step_breakdown_per_op(
1065 &self,
1066 ctx_tokens: u32,
1067 gen_tokens: u32,
1068 isl: u32,
1069 osl: u32,
1070 prefix: u32,
1071 seq_imbalance_correction_scale: f64,
1072 gen_seq_imbalance_correction_scale: f64,
1073 ) -> Result<(Vec<PerOpValue>, Vec<PerOpValue>, Vec<PerOpValue>), AicError> {
1074 let (shared, context_attention, decode_attention) = self.mixed_step_breakdown_per_op_impl(
1075 ctx_tokens,
1076 gen_tokens,
1077 isl,
1078 osl,
1079 prefix,
1080 seq_imbalance_correction_scale,
1081 gen_seq_imbalance_correction_scale,
1082 )?;
1083 Ok((
1084 strip_per_op_metadata(shared),
1085 strip_per_op_metadata(context_attention),
1086 strip_per_op_metadata(decode_attention),
1087 ))
1088 }
1089
1090 #[allow(clippy::too_many_arguments)]
1093 pub(crate) fn mixed_step_breakdown_per_op_with_metadata(
1094 &self,
1095 ctx_tokens: u32,
1096 gen_tokens: u32,
1097 isl: u32,
1098 osl: u32,
1099 prefix: u32,
1100 seq_imbalance_correction_scale: f64,
1101 gen_seq_imbalance_correction_scale: f64,
1102 ) -> Result<MixedStepPerOpValuesWithMetadata, AicError> {
1103 self.mixed_step_breakdown_per_op_impl(
1104 ctx_tokens,
1105 gen_tokens,
1106 isl,
1107 osl,
1108 prefix,
1109 seq_imbalance_correction_scale,
1110 gen_seq_imbalance_correction_scale,
1111 )
1112 }
1113
1114 #[allow(clippy::too_many_arguments)]
1115 fn mixed_step_breakdown_per_op_impl(
1116 &self,
1117 ctx_tokens: u32,
1118 gen_tokens: u32,
1119 isl: u32,
1120 osl: u32,
1121 prefix: u32,
1122 seq_imbalance_correction_scale: f64,
1123 gen_seq_imbalance_correction_scale: f64,
1124 ) -> Result<MixedStepPerOpValuesWithMetadata, AicError> {
1125 if let Some((prefill_op, decode_op)) = self.fpm_ops() {
1132 let (prefill_ms, marginal_decode_ms) = self.fpm_mixed_step_components(
1133 prefill_op,
1134 decode_op,
1135 ctx_tokens,
1136 gen_tokens,
1137 isl.max(1),
1138 osl.max(1),
1139 prefix,
1140 )?;
1141 let mut shared: Vec<PerOpValueWithMetadata> = Vec::new();
1142 if ctx_tokens > 0 {
1143 shared.push((prefill_op.name.clone(), prefill_ms, 0.0, "silicon", None));
1144 }
1145 let mut dec_attn: Vec<PerOpValueWithMetadata> = Vec::new();
1146 if gen_tokens > 0 {
1147 dec_attn.push((
1148 decode_op.name.clone(),
1149 marginal_decode_ms,
1150 0.0,
1151 "silicon",
1152 None,
1153 ));
1154 }
1155 return Ok((shared, Vec::new(), dec_attn));
1156 }
1157 let mut shared = PerOpFold::new("context");
1158 let mut ctx_attn = PerOpFold::new("context");
1159 let mut dec_attn = PerOpFold::new("generation");
1160 self.mixed_step_breakdown_with(
1161 ctx_tokens,
1162 gen_tokens,
1163 isl,
1164 osl,
1165 prefix,
1166 seq_imbalance_correction_scale,
1167 gen_seq_imbalance_correction_scale,
1168 |pass, op, r| {
1169 let out = match pass {
1170 MixedPass::SharedNonAttention => &mut shared,
1171 MixedPass::ContextAttention => &mut ctx_attn,
1172 MixedPass::DecodeAttention => &mut dec_attn,
1173 };
1174 out.add(op, r);
1175 },
1176 )?;
1177 let mut ctx_attn = ctx_attn.into_values();
1178 if ctx_tokens > 0 {
1179 let scale2 = isl.max(1).div_ceil(ctx_tokens) as f64;
1183 for entry in &mut ctx_attn {
1184 entry.1 /= scale2;
1185 entry.2 /= scale2;
1186 }
1187 }
1188 Ok((shared.into_values(), ctx_attn, dec_attn.into_values()))
1189 }
1190
1191 pub fn decode_step_per_op(
1193 &self,
1194 gen_tokens: u32,
1195 isl: u32,
1196 osl: u32,
1197 gen_seq_imbalance_correction_scale: f64,
1198 ) -> Result<Vec<PerOpValue>, AicError> {
1199 self.decode_step_per_op_impl(gen_tokens, isl, osl, gen_seq_imbalance_correction_scale)
1200 .map(strip_per_op_metadata)
1201 }
1202
1203 pub(crate) fn decode_step_per_op_with_metadata(
1206 &self,
1207 gen_tokens: u32,
1208 isl: u32,
1209 osl: u32,
1210 gen_seq_imbalance_correction_scale: f64,
1211 ) -> Result<Vec<PerOpValueWithMetadata>, AicError> {
1212 self.decode_step_per_op_impl(gen_tokens, isl, osl, gen_seq_imbalance_correction_scale)
1213 }
1214
1215 fn decode_step_per_op_impl(
1216 &self,
1217 gen_tokens: u32,
1218 isl: u32,
1219 osl: u32,
1220 gen_seq_imbalance_correction_scale: f64,
1221 ) -> Result<Vec<PerOpValueWithMetadata>, AicError> {
1222 let mut out = PerOpFold::new("generation");
1223 if gen_tokens == 0 {
1224 return Ok(out.into_values());
1225 }
1226 let effective_batch = gen_tokens.saturating_mul(self.nextn.saturating_add(1));
1227 let s = isl.max(1).saturating_add(osl.max(1) / 2).saturating_add(1);
1228 run_generation_ops_step_beamed_with(
1229 &self.generation_ops,
1230 &self.db,
1231 effective_batch,
1232 1,
1233 s,
1234 gen_seq_imbalance_correction_scale,
1235 false,
1236 |op, r| out.add(op, r),
1237 )?;
1238 Ok(out.into_values())
1239 }
1240
1241 #[allow(clippy::too_many_arguments)]
1246 pub fn evaluate_context_ops(
1247 &self,
1248 indices: &[usize],
1249 batch_size: u32,
1250 s: u32,
1251 prefix: u32,
1252 seq_imbalance_correction_scale: f64,
1253 x_override: Option<u32>,
1254 ) -> Result<Vec<PerOpValue>, AicError> {
1255 let mut out = PerOpFold::new("context");
1256 for &i in indices {
1257 let op = self.context_ops.get(i).ok_or_else(|| {
1258 AicError::InvalidEngineConfig(format!(
1259 "evaluate_context_ops: index {i} out of range ({} context ops)",
1260 self.context_ops.len()
1261 ))
1262 })?;
1263 let r = query_context_op(
1264 op,
1265 &self.db,
1266 batch_size,
1267 s,
1268 prefix,
1269 seq_imbalance_correction_scale,
1270 x_override,
1271 )?;
1272 out.add(op, r);
1273 }
1274 Ok(strip_per_op_metadata(out.into_values()))
1275 }
1276
1277 #[allow(clippy::too_many_arguments)]
1280 pub fn evaluate_generation_ops(
1281 &self,
1282 indices: &[usize],
1283 batch_size: u32,
1284 s: u32,
1285 gen_seq_imbalance_correction_scale: f64,
1286 prefix: u32,
1287 x_override: Option<u32>,
1288 ) -> Result<Vec<PerOpValue>, AicError> {
1289 let mut out = PerOpFold::new("generation");
1290 for &i in indices {
1291 let op = self.generation_ops.get(i).ok_or_else(|| {
1292 AicError::InvalidEngineConfig(format!(
1293 "evaluate_generation_ops: index {i} out of range ({} generation ops)",
1294 self.generation_ops.len()
1295 ))
1296 })?;
1297 let r = query_generation_op(
1298 op,
1299 &self.db,
1300 batch_size,
1301 1,
1302 s,
1303 gen_seq_imbalance_correction_scale,
1304 prefix,
1305 x_override,
1306 )?;
1307 out.add(op, r);
1308 }
1309 Ok(strip_per_op_metadata(out.into_values()))
1310 }
1311
1312 #[allow(clippy::too_many_arguments)]
1317 pub fn evaluate_ops_json(
1318 &self,
1319 ops_json: &str,
1320 is_context: bool,
1321 batch_size: u32,
1322 s: u32,
1323 prefix: u32,
1324 imbalance_correction_scale: f64,
1325 x_override: Option<u32>,
1326 ) -> Result<Vec<PerOpValue>, AicError> {
1327 let ops: Vec<Op> = serde_json::from_str(ops_json).map_err(|e| {
1328 AicError::InvalidEngineConfig(format!("evaluate_ops_json: invalid op list JSON: {e}"))
1329 })?;
1330 let mut out = PerOpFold::new(if is_context { "context" } else { "generation" });
1331 for op in &ops {
1332 let r = if is_context {
1333 query_context_op(
1334 op,
1335 &self.db,
1336 batch_size,
1337 s,
1338 prefix,
1339 imbalance_correction_scale,
1340 x_override,
1341 )?
1342 } else {
1343 query_generation_op(
1344 op,
1345 &self.db,
1346 batch_size,
1347 1,
1348 s,
1349 imbalance_correction_scale,
1350 prefix,
1351 x_override,
1352 )?
1353 };
1354 out.add(op, r);
1355 }
1356 Ok(strip_per_op_metadata(out.into_values()))
1357 }
1358
1359 #[allow(clippy::too_many_arguments)]
1367 pub fn evaluate_ops_sol_json(
1368 &self,
1369 ops_json: &str,
1370 is_context: bool,
1371 batch_size: u32,
1372 s: u32,
1373 prefix: u32,
1374 imbalance_correction_scale: f64,
1375 x_override: Option<u32>,
1376 ) -> Result<Vec<PerOpSolValue>, AicError> {
1377 let ops: Vec<Op> = serde_json::from_str(ops_json).map_err(|e| {
1378 AicError::InvalidEngineConfig(format!(
1379 "evaluate_ops_sol_json: invalid op list JSON: {e}"
1380 ))
1381 })?;
1382 let sol_db = self.db.sol_full_view();
1383 let mut out = PerOpSolFold::default();
1384 for op in &ops {
1385 let r = if is_context {
1386 query_context_op(
1387 op,
1388 &sol_db,
1389 batch_size,
1390 s,
1391 prefix,
1392 imbalance_correction_scale,
1393 x_override,
1394 )?
1395 } else {
1396 query_generation_op(
1397 op,
1398 &sol_db,
1399 batch_size,
1400 1,
1401 s,
1402 imbalance_correction_scale,
1403 prefix,
1404 x_override,
1405 )?
1406 };
1407 out.add(op, r)?;
1408 }
1409 Ok(out.into_values())
1410 }
1411
1412 pub fn forward_pass_time_ms(
1428 &self,
1429 metrics_by_rank: &[ForwardPassMetrics],
1430 ) -> Result<f64, AicError> {
1431 if metrics_by_rank.is_empty() {
1432 return Err(AicError::InvalidForwardPassMetrics(
1433 "at least one attention-DP rank metric required".to_string(),
1434 ));
1435 }
1436 for metrics in metrics_by_rank {
1437 validate_forward_pass_metrics(metrics)?;
1438 }
1439 let mut max_latency = 0.0_f64;
1440 for metrics in metrics_by_rank {
1441 let rank_latency = self.rank_latency_ms(metrics)?;
1442 if rank_latency > max_latency {
1443 max_latency = rank_latency;
1444 }
1445 }
1446 Ok(max_latency)
1447 }
1448
1449 fn rank_latency_ms(&self, metrics: &ForwardPassMetrics) -> Result<f64, AicError> {
1456 let sched = &metrics.scheduled_requests;
1457 let has_prefill = sched.sum_prefill_tokens > 0;
1464 let has_decode = sched.num_decode_requests > 0 || sched.sum_decode_kv_tokens > 0;
1465
1466 if let Some((prefill_op, decode_op)) = self.fpm_ops() {
1474 let mut total = 0.0_f64;
1480 if has_prefill {
1481 total += prefill_op
1482 .query_totals(
1483 &self.db,
1484 &[
1485 sched.num_prefill_requests as f64,
1486 sched.sum_prefill_tokens as f64,
1487 sched.sum_prefill_kv_tokens as f64,
1488 ],
1489 )?
1490 .latency_ms;
1491 }
1492 if has_decode {
1493 let decode_ms = decode_op
1494 .query_totals(
1495 &self.db,
1496 &[
1497 sched.num_decode_requests as f64,
1498 sched.sum_decode_kv_tokens as f64,
1499 ],
1500 )?
1501 .latency_ms;
1502 if has_prefill {
1503 let baseline_ms = decode_op
1507 .query_pass_baseline(&self.db, sched.num_decode_requests)?
1508 .latency_ms;
1509 total += (decode_ms - baseline_ms).max(0.0);
1510 } else {
1511 total += decode_ms;
1512 }
1513 }
1514 return Ok(total);
1515 }
1516
1517 if has_prefill && has_decode {
1518 let n_prefill = sched.num_prefill_requests.max(1);
1523 let new_tokens_per_req = sched.sum_prefill_tokens / n_prefill;
1524 let prefix_per_req = sched.sum_prefill_kv_tokens / n_prefill;
1525 let n_decode = sched.num_decode_requests.max(1);
1526 let kv_per_req = sched.sum_decode_kv_tokens / n_decode;
1527 let ctx_tokens = sched.sum_prefill_tokens;
1528 let gen_tokens = sched.num_decode_requests;
1529 return get_mix_step_ops(
1530 &self.context_ops,
1531 &self.generation_ops,
1532 &self.db,
1533 ctx_tokens,
1534 gen_tokens,
1535 new_tokens_per_req.max(1),
1536 prefix_per_req,
1537 sched.sum_prefill_kv_tokens,
1538 kv_per_req,
1539 n_decode,
1540 );
1541 }
1542
1543 let mut total = 0.0_f64;
1544
1545 if has_prefill {
1546 let n_prefill = sched.num_prefill_requests.max(1);
1547 let new_tokens_per_req = sched.sum_prefill_tokens / n_prefill;
1548 let prefix_per_req = sched.sum_prefill_kv_tokens / n_prefill;
1549 total += run_context_ops(
1550 &self.context_ops,
1551 &self.db,
1552 n_prefill,
1553 new_tokens_per_req,
1554 prefix_per_req,
1555 1.0,
1556 ContextOpFilter::All,
1557 )?;
1558 }
1559
1560 if has_decode {
1561 let n_decode = sched.num_decode_requests.max(1);
1562 let kv_per_req = sched.sum_decode_kv_tokens / n_decode;
1563 total += run_generation_ops_step(
1564 &self.generation_ops,
1565 &self.db,
1566 n_decode,
1567 kv_per_req,
1568 1.0,
1569 false,
1570 )?;
1571 }
1572
1573 Ok(total)
1574 }
1575}
1576
1577#[cfg(test)]
1578mod tests {
1579 use super::*;
1580 use std::collections::BTreeMap;
1581 use std::path::PathBuf;
1582
1583 use crate::common::enums::{FmhaQuantMode, GemmQuantMode, KvCacheQuantMode};
1584 use crate::engine::spec::EngineSpec;
1585 use crate::operators::op::Op;
1586 use crate::operators::{
1587 ContextAttentionOp, ElementwiseOp, GemmOp, GenerationAttentionOp, MoeAllToAllOp,
1588 };
1589 use crate::{BackendKind, EngineConfig, ParallelMapping, QuantizationConfig};
1590
1591 fn systems_root() -> PathBuf {
1592 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../src/aiconfigurator_core/systems")
1593 }
1594
1595 const TEST_MODEL: &str = "MiniMaxAI/MiniMax-M2.5";
1596
1597 fn context_ops() -> Vec<Op> {
1602 vec![
1603 Op::Elementwise(ElementwiseOp {
1604 name: "rmsnorm".into(),
1605 scale_factor: 1.0,
1606 bytes_per_token: 8192.0,
1607 scale_num_tokens: 1,
1608 seq_split: 1,
1609 }),
1610 Op::Gemm(GemmOp {
1611 name: "qkv_gemm".into(),
1612 scale_factor: 1.0,
1613 n: 4096,
1614 k: 4096,
1615 quant_mode: GemmQuantMode::Fp8Block,
1616 scale_num_tokens: 0,
1617 low_precision_input: false,
1618 seq_split: 1,
1619 below_grid_sol: false,
1620 }),
1621 Op::ContextAttention(ContextAttentionOp {
1622 name: "context_attention".into(),
1623 scale_factor: 1.0,
1624 n: 32,
1625 n_kv: 8,
1626 head_size: 128,
1627 window_size: 0,
1628 kv_cache_dtype: KvCacheQuantMode::Fp8,
1629 fmha_quant_mode: FmhaQuantMode::Bfloat16,
1630 use_qk_norm: false,
1631 cp_size: 1,
1632 lane_order: crate::operators::attention::b200_vllm_context_lane_order(),
1633 }),
1634 ]
1635 }
1636
1637 fn generation_ops() -> Vec<Op> {
1638 vec![
1639 Op::Elementwise(ElementwiseOp {
1640 name: "rmsnorm".into(),
1641 scale_factor: 1.0,
1642 bytes_per_token: 8192.0,
1643 scale_num_tokens: 1,
1644 seq_split: 1,
1645 }),
1646 Op::GenerationAttention(GenerationAttentionOp {
1647 name: "generation_attention".into(),
1648 scale_factor: 1.0,
1649 n: 32,
1650 n_kv: 8,
1651 head_size: 128,
1652 window_size: 0,
1653 kv_cache_dtype: KvCacheQuantMode::Fp8,
1654 lane_order: crate::operators::attention::b200_vllm_generation_lane_order(),
1655 }),
1656 ]
1657 }
1658
1659 fn fixture_engine_config(nextn: Option<u32>) -> EngineConfig {
1660 EngineConfig {
1661 schema_version: crate::ENGINE_CONFIG_SCHEMA_VERSION,
1662 model_name: TEST_MODEL.to_string(),
1663 system_name: "b200_sxm".to_string(),
1664 systems_path: None,
1665 backend: BackendKind::Vllm,
1666 backend_version: Some("0.24.0".to_string()),
1667 forward_model: None,
1668 kv_block_size: None,
1669 parallel: ParallelMapping {
1670 tp_size: 8,
1671 pp_size: 1,
1672 attention_dp_size: Some(1),
1673 moe_tp_size: Some(1),
1674 moe_ep_size: Some(8),
1675 cp_size: None,
1676 },
1677 quantization: QuantizationConfig {
1678 weight_dtype: None,
1679 moe_dtype: None,
1680 activation_dtype: None,
1681 kv_cache_dtype: None,
1682 },
1683 speculative: nextn.map(|n| crate::SpeculativeConfig { nextn: Some(n) }),
1684 enable_shared_layer: None,
1685 strict_provenance: false,
1686 tolerate_dirless_version: false,
1687 database_mode: Default::default(),
1688 transfer_policy: None,
1689 extra: BTreeMap::new(),
1690 }
1691 }
1692
1693 fn build_engine(nextn: Option<u32>) -> Engine {
1695 let db = PerfDatabase::load(&systems_root(), "b200_sxm", "vllm", "0.24.0").unwrap();
1696 let spec = EngineSpec::new(
1697 fixture_engine_config(nextn),
1698 context_ops(),
1699 generation_ops(),
1700 );
1701 Engine::build(spec, Arc::new(db)).unwrap()
1702 }
1703
1704 fn runtime(batch_size: u32, isl: u32, osl: u32) -> RuntimeConfig {
1705 RuntimeConfig {
1706 batch_size,
1707 isl,
1708 osl,
1709 ..Default::default()
1710 }
1711 }
1712
1713 #[test]
1714 fn per_op_fold_attaches_the_inference_phase_only_to_executed_fallbacks() {
1715 use crate::operators::base::{MoeCommFallback, Source};
1716
1717 let op = context_ops().remove(0);
1718 let fallback = MoeCommFallback {
1719 comm_backend: "deepep_ht",
1720 requested_ep_size: 32,
1721 requested_node_num: 8,
1722 measurement_ep_size: 8,
1723 measurement_node_num: 1,
1724 };
1725 for inference_phase in ["context", "generation"] {
1726 let mut fold = PerOpFold::new(inference_phase);
1727 fold.add(
1728 &op,
1729 PerformanceResult::new(1.0, Source::Estimated).with_moe_comm_fallback(fallback),
1730 );
1731 assert_eq!(
1732 fold.into_values()[0].4,
1733 Some(((inference_phase, "deepep_ht", 32, 8, 8, 1), vec![]))
1734 );
1735 }
1736
1737 let mut repeated_name = PerOpFold::new("context");
1738 repeated_name.add(
1739 &op,
1740 PerformanceResult::new(1.0, Source::Estimated).with_moe_comm_fallback(fallback),
1741 );
1742 repeated_name.add(
1743 &op,
1744 PerformanceResult::new(1.0, Source::Estimated).with_moe_comm_fallback(
1745 MoeCommFallback {
1746 comm_backend: "deepep_ll",
1747 ..fallback
1748 },
1749 ),
1750 );
1751 assert_eq!(
1752 repeated_name.into_values()[0].4,
1753 Some((
1754 ("context", "deepep_ht", 32, 8, 8, 1),
1755 vec![("context", "deepep_ll", 32, 8, 8, 1)],
1756 ))
1757 );
1758
1759 let mut exact = PerOpFold::new("context");
1760 exact.add(&op, PerformanceResult::new(1.0, Source::Silicon));
1761 assert_eq!(exact.into_values()[0].4, None);
1762 }
1763
1764 #[test]
1765 fn per_op_fold_allocates_additional_storage_only_for_distinct_fallbacks_after_the_first() {
1766 use crate::operators::base::{MoeCommFallback, Source};
1767
1768 let op = context_ops().remove(0);
1769 let ht = MoeCommFallback {
1770 comm_backend: "deepep_ht",
1771 requested_ep_size: 32,
1772 requested_node_num: 8,
1773 measurement_ep_size: 8,
1774 measurement_node_num: 1,
1775 };
1776 let ll = MoeCommFallback {
1777 comm_backend: "deepep_ll",
1778 ..ht
1779 };
1780
1781 let mut empty = PerOpFold::new("context");
1782 empty.add(&op, PerformanceResult::new(1.0, Source::Silicon));
1783 assert!(empty.into_values().pop().unwrap().4.is_none());
1784
1785 let mut single = PerOpFold::new("context");
1786 single.add(
1787 &op,
1788 PerformanceResult::new(1.0, Source::Estimated).with_moe_comm_fallback(ht),
1789 );
1790 let (first, additional) = single.into_values().pop().unwrap().4.unwrap();
1791 assert_eq!(first, ("context", "deepep_ht", 32, 8, 8, 1));
1792 assert_eq!(additional.capacity(), 0);
1793
1794 let mut multiple = PerOpFold::new("generation");
1795 for fallback in [ht, ht, ll, ll] {
1796 multiple.add(
1797 &op,
1798 PerformanceResult::new(1.0, Source::Estimated).with_moe_comm_fallback(fallback),
1799 );
1800 }
1801 let (first, additional) = multiple.into_values().pop().unwrap().4.unwrap();
1802 assert_eq!(first, ("generation", "deepep_ht", 32, 8, 8, 1));
1803 assert_eq!(additional, vec![("generation", "deepep_ll", 32, 8, 8, 1)]);
1804 }
1805
1806 #[test]
1807 fn generation_step_preserves_distinct_same_name_deepep_fallbacks() {
1808 let mut config = fixture_engine_config(None);
1809 config.system_name = "gb200".to_string();
1810 config.backend = BackendKind::Sglang;
1811 config.backend_version = Some("0.5.16".to_string());
1812
1813 let a2a = |moe_ep_size, node_num| {
1814 Op::MoeAllToAll(MoeAllToAllOp {
1815 name: "generation_moe_dispatch".to_string(),
1816 scale_factor: 1.0,
1817 phase: "dispatch".to_string(),
1818 comm_backend: "deepep_ll".to_string(),
1819 comm_dtype: "default".to_string(),
1820 hidden_size: 7168,
1821 topk: 8,
1822 num_experts: 256,
1823 moe_ep_size,
1824 node_num,
1825 sms: 0,
1826 attention_tp_size: 1,
1827 })
1828 };
1829 let spec = EngineSpec::new(config, Vec::new(), vec![a2a(32, 8), a2a(64, 16)]);
1830 let engine = Engine::from_spec_bytes(&spec.to_bincode().unwrap(), &systems_root())
1831 .expect("shipped GB200 SGLang DeepEP data must load");
1832 let runtime = RuntimeConfig {
1833 batch_size: 1,
1834 isl: 1024,
1835 osl: 2,
1836 ..Default::default()
1837 };
1838
1839 let (_, generation) = engine
1840 .run_static_per_op_with_metadata(&runtime, StaticMode::Generation, 32)
1841 .unwrap();
1842 assert_eq!(generation.len(), 1, "same-name ops must remain name-folded");
1843 assert_eq!(
1844 generation[0].4,
1845 Some((
1846 ("generation", "deepep_ll", 32, 8, 8, 1),
1847 vec![("generation", "deepep_ll", 64, 16, 8, 1)],
1848 ))
1849 );
1850 }
1851
1852 #[test]
1853 fn from_spec_bytes_shares_parsed_tables_across_engines() {
1854 use crate::operators::util_empirical::ProvenanceTier;
1855
1856 let spec1 = EngineSpec::new(fixture_engine_config(None), context_ops(), generation_ops());
1859 let spec2 = EngineSpec::new(
1860 fixture_engine_config(Some(1)),
1861 context_ops(),
1862 generation_ops(),
1863 );
1864 let e1 = Engine::from_spec_bytes(&spec1.to_bincode().unwrap(), &systems_root()).unwrap();
1865 let e2 = Engine::from_spec_bytes(&spec2.to_bincode().unwrap(), &systems_root()).unwrap();
1866 assert!(
1867 std::sync::Arc::ptr_eq(e1.database().tables_arc(), e2.database().tables_arc()),
1868 "engines over the same db identity must share parsed tables"
1869 );
1870 e1.database().note_provenance(ProvenanceTier::Empirical);
1873 assert_eq!(e2.database().worst_provenance(), ProvenanceTier::Silicon);
1874 }
1875
1876 #[test]
1877 fn both_equals_context_plus_generation() {
1878 let engine = build_engine(None);
1879 let rt = runtime(1, 1024, 8);
1880 let both = engine.run_static(&rt, StaticMode::Both, 32).unwrap();
1881 let ctx = engine.run_static(&rt, StaticMode::Context, 32).unwrap();
1882 let gen = engine.run_static(&rt, StaticMode::Generation, 32).unwrap();
1883
1884 assert!((both.context_ms - ctx.context_ms).abs() < 1e-9);
1885 assert!((both.generation_ms - gen.generation_ms).abs() < 1e-9);
1886 assert!((both.total_ms - (ctx.context_ms + gen.generation_ms)).abs() < 1e-9);
1887 assert!((both.total_ms - (ctx.total_ms + gen.total_ms)).abs() < 1e-9);
1889 }
1890
1891 #[test]
1892 fn context_mode_has_zero_generation() {
1893 let engine = build_engine(None);
1894 let rt = runtime(1, 1024, 8);
1895 let ctx = engine.run_static(&rt, StaticMode::Context, 32).unwrap();
1896 assert!(ctx.context_ms > 0.0, "context latency must be non-trivial");
1897 assert_eq!(ctx.generation_ms, 0.0);
1898 assert_eq!(ctx.total_ms, ctx.context_ms);
1899 }
1900
1901 #[test]
1902 fn generation_mode_has_zero_context() {
1903 let engine = build_engine(None);
1904 let rt = runtime(1, 1024, 8);
1905 let gen = engine.run_static(&rt, StaticMode::Generation, 32).unwrap();
1906 assert!(
1907 gen.generation_ms > 0.0,
1908 "generation latency must be non-trivial"
1909 );
1910 assert_eq!(gen.context_ms, 0.0);
1911 assert_eq!(gen.total_ms, gen.generation_ms);
1912 }
1913
1914 #[test]
1915 fn stride_honored() {
1916 let engine = build_engine(None);
1917 let rt = runtime(1, 1024, 9);
1922 let fine = engine.run_static(&rt, StaticMode::Generation, 1).unwrap();
1923 let coarse = engine.run_static(&rt, StaticMode::Generation, 32).unwrap();
1924 assert!(fine.generation_ms > 0.0 && coarse.generation_ms > 0.0);
1925 assert!(
1926 (fine.generation_ms - coarse.generation_ms).abs() > 1e-9,
1927 "stride=1 ({}) and stride=32 ({}) must differ for osl=9",
1928 fine.generation_ms,
1929 coarse.generation_ms
1930 );
1931
1932 let one_step = run_generation_ops_step(
1935 &engine.generation_ops,
1936 engine.database(),
1937 1, 1024 + 0 + 1,
1939 1.0,
1940 false,
1941 )
1942 .unwrap();
1943 assert!((coarse.generation_ms - one_step * 8.0).abs() < 1e-6);
1944 }
1945
1946 #[test]
1947 fn osl_one_yields_zero_generation() {
1948 let engine = build_engine(None);
1949 let rt = runtime(1, 1024, 1);
1950 let gen = engine.run_static(&rt, StaticMode::Generation, 32).unwrap();
1951 assert_eq!(gen.generation_ms, 0.0);
1952 }
1953
1954 #[test]
1955 fn prefix_ge_isl_errors() {
1956 let engine = build_engine(None);
1957 let rt = RuntimeConfig {
1958 batch_size: 1,
1959 isl: 512,
1960 osl: 2,
1961 prefix: 512,
1962 ..Default::default()
1963 };
1964 assert!(engine.run_static(&rt, StaticMode::Context, 32).is_err());
1965 }
1966
1967 #[test]
1968 fn mixed_step_empty_is_zero() {
1969 let engine = build_engine(None);
1970 assert_eq!(
1971 engine
1972 .mixed_step_latency(0, 0, 1024, 8, 0, 1.0, 1.0)
1973 .unwrap(),
1974 0.0
1975 );
1976 }
1977
1978 #[test]
1979 fn mixed_step_nonempty_is_positive() {
1980 let engine = build_engine(None);
1985 let ms = engine
1986 .mixed_step_latency(1024, 2, 1024, 8, 0, 1.0, 1.0)
1987 .unwrap();
1988 assert!(
1989 ms > 0.0 && ms.is_finite(),
1990 "mixed-step latency must be > 0, got {ms}"
1991 );
1992 let breakdown = engine
1993 .mixed_step_breakdown(1024, 2, 1024, 8, 0, 1.0, 1.0)
1994 .unwrap();
1995 assert_eq!(breakdown[0], breakdown[1] + breakdown[2] + breakdown[3]);
1996 assert_eq!(ms, breakdown[0]);
1997 }
1998
1999 fn build_fpm_engine(tmp: &std::path::Path, nextn: Option<u32>) -> Result<Engine, AicError> {
2005 use crate::perf_database::fpm_forward::tests::{
2006 default_identity, default_rows, write_pair,
2007 };
2008 write_pair(tmp, &default_rows());
2009 let mut db = PerfDatabase::load(&systems_root(), "b200_sxm", "vllm", "0.24.0").unwrap();
2010 db.set_fpm_forward_for_test(crate::perf_database::FpmForwardTable::new(
2011 tmp.to_path_buf(),
2012 "b200_sxm",
2013 "vllm",
2014 "0.25.1",
2015 ));
2016 let fpm_op = |phase: FpmPhase| {
2017 Op::FpmForward(FpmForwardOp {
2018 name: format!("fpm_forward_{}", phase.as_str()),
2019 phase,
2020 model_path: "org/model-a".to_string(),
2021 match_identity: default_identity(4),
2022 weight_bytes: 0.0,
2023 sol_ops: vec![],
2024 })
2025 };
2026 let spec = EngineSpec::new(
2027 fixture_engine_config(nextn),
2028 vec![fpm_op(FpmPhase::Prefill)],
2029 vec![fpm_op(FpmPhase::Decode)],
2030 );
2031 Engine::build(spec, Arc::new(db))
2032 }
2033
2034 #[test]
2035 fn fpm_build_rejects_mtp_and_bad_shape() {
2036 let tmp = tempfile::tempdir().unwrap();
2037 let err = build_fpm_engine(tmp.path(), Some(1)).unwrap_err();
2038 assert!(err.to_string().contains("MTP"), "{err}");
2039
2040 use crate::perf_database::fpm_forward::tests::default_identity;
2042 let db = PerfDatabase::load(&systems_root(), "b200_sxm", "vllm", "0.24.0").unwrap();
2043 let fpm_op = Op::FpmForward(FpmForwardOp {
2044 name: "fpm_forward_prefill".into(),
2045 phase: FpmPhase::Prefill,
2046 model_path: "org/model-a".into(),
2047 match_identity: default_identity(4),
2048 weight_bytes: 0.0,
2049 sol_ops: vec![],
2050 });
2051 let spec = EngineSpec::new(
2052 fixture_engine_config(None),
2053 vec![fpm_op, context_ops().remove(0)],
2054 generation_ops(),
2055 );
2056 let err = Engine::build(spec, Arc::new(db)).unwrap_err();
2057 assert!(err.to_string().contains("exactly one FpmForward"), "{err}");
2058 }
2059
2060 #[test]
2065 fn fpm_mixed_step_is_prefill_plus_marginal_decode() {
2066 let tmp = tempfile::tempdir().unwrap();
2067 let engine = build_fpm_engine(tmp.path(), None).unwrap();
2068 let ms = engine
2074 .mixed_step_latency(2048, 8, 2048, 0, 0, 1.0, 1.0)
2075 .unwrap();
2076 let pre = 20.0 + (40.0 - 20.0) * (2056.0 - 2048.0) / (4096.0 - 2048.0);
2077 let w = (16392.0 - 4096.0) / (65536.0 - 4096.0);
2078 let decode = 7.0 + (9.0 - 7.0) * w;
2079 let expected = pre + (decode - 6.0);
2080 assert!((ms - expected).abs() < 1e-9, "got {ms}, want {expected}");
2081 }
2082
2083 fn build_fpm_engine_with_rows(
2086 tmp: &std::path::Path,
2087 rows: &[crate::perf_database::fpm_forward::tests::RowSpec],
2088 ) -> Result<Engine, AicError> {
2089 use crate::perf_database::fpm_forward::tests::{default_identity, write_pair};
2090 write_pair(tmp, rows);
2091 let mut db = PerfDatabase::load(&systems_root(), "b200_sxm", "vllm", "0.24.0").unwrap();
2092 db.set_fpm_forward_for_test(crate::perf_database::FpmForwardTable::new(
2093 tmp.to_path_buf(),
2094 "b200_sxm",
2095 "vllm",
2096 "0.25.1",
2097 ));
2098 let fpm_op = |phase: FpmPhase| {
2099 Op::FpmForward(FpmForwardOp {
2100 name: format!("fpm_forward_{}", phase.as_str()),
2101 phase,
2102 model_path: "org/model-a".to_string(),
2103 match_identity: default_identity(4),
2104 weight_bytes: 0.0,
2105 sol_ops: vec![],
2106 })
2107 };
2108 let spec = EngineSpec::new(
2109 fixture_engine_config(None),
2110 vec![fpm_op(FpmPhase::Prefill)],
2111 vec![fpm_op(FpmPhase::Decode)],
2112 );
2113 Engine::build(spec, Arc::new(db))
2114 }
2115
2116 fn cliff_rows() -> Vec<crate::perf_database::fpm_forward::tests::RowSpec> {
2117 use crate::perf_database::fpm_forward::tests::RowSpec;
2118 let mk = |kind: &'static str, batch: u32, prefill: u32, kv: u32, lat: f64| RowSpec {
2119 workload_kind: kind,
2120 batch_size: batch,
2121 total_prefill_tokens: prefill,
2122 total_kv_read_tokens: kv,
2123 latency_ms: lat,
2124 ..RowSpec::default()
2125 };
2126 vec![
2127 mk("prefill", 1, 2048, 0, 47.0),
2129 mk("prefill", 1, 2049, 0, 99.0),
2130 mk("prefill", 1, 4096, 0, 99.0),
2131 mk("prefill", 1, 1032, 0, 10.0),
2133 mk("prefill", 1, 1032, 1024, 14.0),
2134 mk("decode", 8, 0, 8, 6.0),
2135 mk("decode", 8, 0, 4096, 7.0),
2136 mk("decode", 8, 0, 65536, 9.0),
2137 ]
2138 }
2139
2140 #[test]
2144 fn fpm_mixed_step_total_crosses_the_graph_cliff() {
2145 let tmp = tempfile::tempdir().unwrap();
2146 let engine = build_fpm_engine_with_rows(tmp.path(), &cliff_rows()).unwrap();
2147 let graph = engine
2149 .mixed_step_breakdown(2048, 0, 2048, 0, 0, 1.0, 1.0)
2150 .unwrap();
2151 assert!(
2152 (graph[1] - 47.0).abs() < 1e-9,
2153 "graph-side prefill {}",
2154 graph[1]
2155 );
2156 let eager = engine
2158 .mixed_step_breakdown(2048, 8, 2048, 0, 0, 1.0, 1.0)
2159 .unwrap();
2160 assert!(
2161 (eager[1] - 99.0).abs() < 1e-9,
2162 "eager-side prefill {}",
2163 eager[1]
2164 );
2165 assert!(eager[1] > graph[1] * 2.0 - 1e-9);
2166 }
2167
2168 #[test]
2171 fn fpm_mixed_step_chunks_average_exact_coordinates() {
2172 let tmp = tempfile::tempdir().unwrap();
2173 let engine = build_fpm_engine_with_rows(tmp.path(), &cliff_rows()).unwrap();
2174 let parts = engine
2177 .mixed_step_breakdown(1024, 8, 2048, 0, 0, 1.0, 1.0)
2178 .unwrap();
2179 assert!((parts[1] - 12.0).abs() < 1e-9, "chunk average {}", parts[1]);
2180 }
2181
2182 #[test]
2185 fn fpm_genonly_step_keeps_full_decode() {
2186 let tmp = tempfile::tempdir().unwrap();
2187 let engine = build_fpm_engine(tmp.path(), None).unwrap();
2188 let ms = engine.decode_step_latency(8, 511, 0, 1.0).unwrap();
2191 assert!((ms - 7.0).abs() < 1e-12, "got {ms}");
2192 let mixed = engine
2194 .mixed_step_latency(0, 8, 511, 0, 0, 1.0, 1.0)
2195 .unwrap();
2196 assert!((mixed - 7.0).abs() < 1e-12, "got {mixed}");
2197 assert_eq!(engine.decode_step_latency(0, 511, 0, 1.0).unwrap(), 0.0);
2198 }
2199
2200 #[test]
2206 fn fpm_rank_prefix_cached_payload_is_decode_only() {
2207 use crate::fpm::{ForwardPassMetrics, ScheduledRequestMetrics};
2208 let tmp = tempfile::tempdir().unwrap();
2209 let engine = build_fpm_engine(tmp.path(), None).unwrap();
2210 let metrics = ForwardPassMetrics {
2211 scheduled_requests: ScheduledRequestMetrics {
2212 num_prefill_requests: 1,
2213 sum_prefill_tokens: 0,
2214 sum_prefill_kv_tokens: 4096,
2215 num_decode_requests: 8,
2216 sum_decode_kv_tokens: 4096, ..Default::default()
2218 },
2219 ..Default::default()
2220 };
2221 let ms = engine.forward_pass_time_ms(&[metrics]).unwrap();
2223 assert!((ms - 7.0).abs() < 1e-12, "{ms}");
2224 }
2225
2226 #[test]
2229 fn fpm_rank_latency_marginal_composition() {
2230 use crate::fpm::{ForwardPassMetrics, ScheduledRequestMetrics};
2231 let tmp = tempfile::tempdir().unwrap();
2232 let engine = build_fpm_engine(tmp.path(), None).unwrap();
2233
2234 let mixed = ForwardPassMetrics {
2235 scheduled_requests: ScheduledRequestMetrics {
2236 num_prefill_requests: 2,
2237 sum_prefill_tokens: 2 * 1024,
2238 sum_prefill_kv_tokens: 0,
2239 num_decode_requests: 8,
2240 sum_decode_kv_tokens: 8 * 4096,
2241 ..Default::default()
2242 },
2243 ..Default::default()
2244 };
2245 let w = (32768.0 - 4096.0) / (65536.0 - 4096.0);
2249 let decode = 7.0 + (9.0 - 7.0) * w;
2250 let expected = 21.0 + (decode - 6.0);
2251 let got = engine.forward_pass_time_ms(&[mixed]).unwrap();
2252 assert!((got - expected).abs() < 1e-9, "got {got}, want {expected}");
2253 }
2254
2255 #[test]
2259 fn fpm_rank_uses_iteration_totals_not_averages() {
2260 use crate::fpm::{ForwardPassMetrics, ScheduledRequestMetrics};
2261 let tmp = tempfile::tempdir().unwrap();
2262 let engine = build_fpm_engine(tmp.path(), None).unwrap();
2263
2264 let decode_only = ForwardPassMetrics {
2268 scheduled_requests: ScheduledRequestMetrics {
2269 num_decode_requests: 8,
2270 sum_decode_kv_tokens: 32_773,
2271 ..Default::default()
2272 },
2273 ..Default::default()
2274 };
2275 let w = (32_773.0 - 4096.0) / (65_536.0 - 4096.0);
2276 let expected = 7.0 + (9.0 - 7.0) * w;
2277 let got = engine.forward_pass_time_ms(&[decode_only]).unwrap();
2278 assert!((got - expected).abs() < 1e-9, "got {got}, want {expected}");
2279 }
2280
2281 #[test]
2286 fn nested_fpm_op_is_rejected_at_build() {
2287 use crate::perf_database::fpm_forward::tests::default_identity;
2288 let db = PerfDatabase::load(&systems_root(), "b200_sxm", "vllm", "0.24.0").unwrap();
2289 let hidden = Op::Overlap(crate::operators::OverlapOp::new(
2290 "hidden",
2291 vec![Op::FpmForward(FpmForwardOp {
2292 name: "fpm_forward_prefill".into(),
2293 phase: FpmPhase::Prefill,
2294 model_path: "org/model-a".into(),
2295 match_identity: default_identity(4),
2296 weight_bytes: 0.0,
2297 sol_ops: vec![],
2298 })],
2299 vec![],
2300 ));
2301 let spec = EngineSpec::new(fixture_engine_config(None), vec![hidden], generation_ops());
2302 let err = Engine::build(spec, Arc::new(db)).unwrap_err();
2303 assert!(
2304 err.to_string()
2305 .contains("exactly one FpmForward op per phase"),
2306 "{err}"
2307 );
2308 }
2309
2310 #[test]
2318 fn nextn_scales_decode_batch() {
2319 let engine_nextn1 = build_engine(Some(1));
2320 assert_eq!(engine_nextn1.nextn, 1);
2321
2322 let rt = runtime(1, 1024, 2);
2325 let gen = engine_nextn1
2326 .run_static(&rt, StaticMode::Generation, 32)
2327 .unwrap();
2328 let doubled = run_generation_ops_step(
2329 &engine_nextn1.generation_ops,
2330 engine_nextn1.database(),
2331 2,
2332 1024 + 1,
2333 1.0,
2334 false,
2335 )
2336 .unwrap();
2337 assert!(
2338 (gen.generation_ms - doubled).abs() < 1e-9,
2339 "nextn=1 gen ({}) must equal the gen-step at 2*batch ({})",
2340 gen.generation_ms,
2341 doubled
2342 );
2343 }
2344
2345 #[test]
2352 fn evaluate_ops_sol_json_matches_sol_view() {
2353 use crate::perf_database::gemm::quant_tc_flops;
2354 use crate::session::query_context_op;
2355
2356 let engine = build_engine(None);
2357 let ops = context_ops();
2358 let ops_json = serde_json::to_string(&ops).unwrap();
2359 let (batch, s) = (4u32, 512u32);
2360 let sol = engine
2361 .evaluate_ops_sol_json(&ops_json, true, batch, s, 0, 1.0, None)
2362 .unwrap();
2363 assert_eq!(sol.len(), ops.len());
2364
2365 let sol_db = engine.database().sol_full_view();
2366 for (op, entry) in ops.iter().zip(&sol) {
2367 let r = query_context_op(op, &sol_db, batch, s, 0, 1.0, None).unwrap();
2368 assert_eq!(entry.0, op.name());
2369 assert!(
2370 (entry.1 - r.latency_ms).abs() < 1e-12,
2371 "{}: sol_time {} != Sol-view latency {}",
2372 entry.0,
2373 entry.1,
2374 r.latency_ms
2375 );
2376 }
2377
2378 for entry in sol.iter().take(2) {
2382 assert!(
2383 (entry.1 - entry.2.max(entry.3)).abs() < 1e-12,
2384 "{}: leaf max identity broken: {:?}",
2385 entry.0,
2386 entry
2387 );
2388 }
2389
2390 let spec = &engine.database().system_spec;
2393 let quant = GemmQuantMode::Fp8Block;
2394 let tc_flops = quant_tc_flops(spec, quant.mapping()).unwrap();
2395 let (m, n, k) = ((batch * s) as f64, 4096.0, 4096.0);
2396 let math = 2.0 * m * n * k / tc_flops * 1000.0;
2397 let mem = quant.mapping().memory * (m * n + m * k + n * k) / spec.gpu.mem_bw * 1000.0;
2398 let gemm = &sol[1];
2399 assert!(
2400 (gemm.2 - math).abs() < 1e-12,
2401 "sol_math {} != {math}",
2402 gemm.2
2403 );
2404 assert!((gemm.3 - mem).abs() < 1e-12, "sol_mem {} != {mem}", gemm.3);
2405 }
2406
2407 #[test]
2412 fn evaluate_ops_sol_json_blends_dsa_full_skip() {
2413 use crate::common::enums::{FmhaQuantMode, KvCacheQuantMode};
2414 use crate::operators::DsaModuleOp;
2415 use crate::perf_database::dsa::{dsa_context_sol, dsa_context_sol_flops, dsa_dims};
2416
2417 let engine = build_engine(None);
2418 let spec = &engine.database().system_spec;
2419 let mut op = DsaModuleOp::new(
2420 "dsa_context",
2421 128,
2422 KvCacheQuantMode::Bfloat16,
2423 FmhaQuantMode::Bfloat16,
2424 GemmQuantMode::Bfloat16,
2425 "DeepseekV32ForCausalLM",
2426 2048,
2427 );
2428 let w = 0.5;
2429 op.full_frac = w;
2430 let (b, s) = (1u32, 4096u32);
2431 let ops_json = serde_json::to_string(&vec![Op::DsaContext(op.clone())]).unwrap();
2432 let sol = engine
2433 .evaluate_ops_sol_json(&ops_json, true, b, s, 0, 1.0, None)
2434 .unwrap();
2435 assert_eq!(sol.len(), 1);
2436
2437 let dims = dsa_dims(&op.architecture);
2438 let flops = dsa_context_sol_flops(spec, op.gemm_quant_mode, op.fmha_quant_mode).unwrap();
2439 let leaf = |skip: bool| {
2440 dsa_context_sol(
2441 spec,
2442 dims,
2443 op.index_topk as i64,
2444 op.kv_cache_dtype,
2445 op.fmha_quant_mode,
2446 op.gemm_quant_mode,
2447 b as i64,
2448 s as i64,
2449 0,
2450 op.num_heads as i64,
2451 skip,
2452 flops,
2453 )
2454 };
2455 let (full, skip) = (leaf(false), leaf(true));
2456 let expected_math = w * full.math_ms + (1.0 - w) * skip.math_ms;
2457 let expected_mem = w * full.mem_ms + (1.0 - w) * skip.mem_ms;
2458 let expected_time = w * full.time_ms() + (1.0 - w) * skip.time_ms();
2459 let (_, sol_time, sol_math, sol_mem) = &sol[0];
2460 assert!(
2461 (sol_time - expected_time).abs() < 1e-12,
2462 "{sol_time} vs {expected_time}"
2463 );
2464 assert!(
2465 (sol_math - expected_math).abs() < 1e-12,
2466 "{sol_math} vs {expected_math}"
2467 );
2468 assert!(
2469 (sol_mem - expected_mem).abs() < 1e-12,
2470 "{sol_mem} vs {expected_mem}"
2471 );
2472 assert!(skip.time_ms() < full.time_ms());
2475 }
2476
2477 #[test]
2483 fn evaluate_ops_sol_json_rejects_cp_dsa_explicitly() {
2484 use crate::operators::DsaModuleOp;
2485
2486 let engine = build_engine(None);
2487 let mut op = DsaModuleOp::new(
2488 "dsa_context",
2489 64,
2490 KvCacheQuantMode::Bfloat16,
2491 FmhaQuantMode::Bfloat16,
2492 GemmQuantMode::Bfloat16,
2493 "GlmMoeDsaForCausalLM",
2494 2048,
2495 );
2496 op.cp_size = 2;
2497 op.full_frac = 0.5;
2498 let ops_json = serde_json::to_string(&vec![Op::DsaContext(op)]).unwrap();
2499 let err = engine
2500 .evaluate_ops_sol_json(&ops_json, true, 1, 4096, 0, 1.0, None)
2501 .unwrap_err();
2502
2503 match err {
2504 AicError::InvalidEngineConfig(message) => {
2505 assert!(
2506 message.contains("DSA context SOL_FULL decomposition is not supported")
2507 && message.contains("cp_size=2")
2508 && message.contains("sparse MQA/top-k deltas are latency-only"),
2509 "unexpected message: {message}"
2510 );
2511 }
2512 other => panic!("expected explicit CP DSA configuration error, got {other}"),
2513 }
2514 }
2515
2516 #[test]
2519 fn evaluate_ops_sol_json_rejects_unexported_families() {
2520 let engine = build_engine(None);
2521 let ops = vec![Op::Mamba2(crate::operators::Mamba2Op {
2522 name: "mamba2".into(),
2523 scale_factor: 1.0,
2524 kernel_source: "causal_conv1d_fn".into(),
2525 phase: "context".into(),
2526 d_model: 4096,
2527 d_state: 128,
2528 d_conv: 4,
2529 nheads: 128,
2530 head_dim: 64,
2531 n_groups: 8,
2532 chunk_size: 256,
2533 })];
2534 let ops_json = serde_json::to_string(&ops).unwrap();
2535 let err = engine
2536 .evaluate_ops_sol_json(&ops_json, true, 1, 128, 0, 1.0, None)
2537 .unwrap_err();
2538 assert!(matches!(&err, AicError::SolNotImplemented(_)));
2539 assert!(
2540 err.to_string().contains("no SOL decomposition"),
2541 "unexpected error: {err}"
2542 );
2543 }
2544}