1use std::sync::Arc;
20
21use crate::common::enums::{DatabaseMode, TransferPolicy};
22use crate::common::error::AicError;
23use crate::operators::base::PerformanceResult;
24use crate::operators::{FpmForwardOp, FpmPhase, Op};
25use crate::perf_database::PerfDatabase;
26use crate::perfmodel::engine::spec::EngineSpec;
27use crate::session::{
28 ContextOpFilter, get_mix_step_ops, query_context_op, query_generation_op, run_context_ops,
29 run_context_ops_with, run_generation_ops_step, run_generation_ops_step_beamed_with,
30};
31use crate::{ForwardPassMetrics, validate_forward_pass_metrics};
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 predict_decode_latency_total(
656 &self,
657 batch_size: u32,
658 total_past_kv_tokens: u32,
659 ) -> Result<f64, AicError> {
660 self.forward_pass_time_ms(&[ForwardPassMetrics {
661 scheduled_requests: crate::ScheduledRequestMetrics {
662 num_decode_requests: batch_size,
663 sum_decode_kv_tokens: total_past_kv_tokens,
664 ..Default::default()
665 },
666 ..Default::default()
667 }])
668 }
669
670 pub fn fpm_decode_kv_ceiling(&self) -> Result<Option<u32>, AicError> {
673 let Some((_prefill, decode)) = self.fpm_ops() else {
674 return Ok(None);
675 };
676 decode.decode_kv_ceiling(&self.db)
677 }
678
679 pub fn mixed_step_latency(
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, AicError> {
722 Ok(self.mixed_step_breakdown(
723 ctx_tokens,
724 gen_tokens,
725 isl,
726 osl,
727 prefix,
728 seq_imbalance_correction_scale,
729 gen_seq_imbalance_correction_scale,
730 )?[0])
731 }
732
733 pub fn mixed_step_breakdown(
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 ) -> Result<[f64; 4], AicError> {
750 self.mixed_step_breakdown_with(
751 ctx_tokens,
752 gen_tokens,
753 isl,
754 osl,
755 prefix,
756 seq_imbalance_correction_scale,
757 gen_seq_imbalance_correction_scale,
758 |_, _, _| {},
759 )
760 }
761
762 #[allow(clippy::too_many_arguments)]
768 fn mixed_step_breakdown_with(
769 &self,
770 ctx_tokens: u32,
771 gen_tokens: u32,
772 isl: u32,
773 osl: u32,
774 prefix: u32,
775 seq_imbalance_correction_scale: f64,
776 gen_seq_imbalance_correction_scale: f64,
777 mut on_op: impl FnMut(MixedPass, &Op, PerformanceResult),
778 ) -> Result<[f64; 4], AicError> {
779 if ctx_tokens == 0 && gen_tokens == 0 {
780 return Ok([0.0; 4]);
781 }
782 if let Some((prefill_op, decode_op)) = self.fpm_ops() {
791 let (prefill_ms, marginal_decode_ms) = self.fpm_mixed_step_components(
792 prefill_op,
793 decode_op,
794 ctx_tokens,
795 gen_tokens,
796 isl.max(1),
797 osl.max(1),
798 prefix,
799 )?;
800 return Ok([
801 prefill_ms + marginal_decode_ms,
802 prefill_ms,
803 0.0,
804 marginal_decode_ms,
805 ]);
806 }
807 let isl = isl.max(1);
811
812 let decode_query_tokens = gen_tokens.saturating_mul(self.nextn.saturating_add(1));
819 let combined = ctx_tokens + decode_query_tokens;
820 let prefix1 = prefix * (ctx_tokens / isl); if prefix1 >= combined {
822 return Err(AicError::InvalidEngineConfig(format!(
823 "isl must be greater than 0 after removing prefix, but got {}",
824 combined as i64 - prefix1 as i64
825 )));
826 }
827 let shared_non_attention = run_context_ops_with(
828 &self.context_ops,
829 &self.db,
830 1,
831 combined - prefix1,
832 prefix1,
833 seq_imbalance_correction_scale,
834 ContextOpFilter::SkipContextAttention,
835 |op, r| on_op(MixedPass::SharedNonAttention, op, r),
836 )?;
837
838 let mut context_attention = 0.0_f64;
843 if ctx_tokens > 0 {
844 if prefix >= isl {
845 return Err(AicError::InvalidEngineConfig(format!(
846 "isl must be greater than 0 after removing prefix, but got {}",
847 isl as i64 - prefix as i64
848 )));
849 }
850 let batch2 = ctx_tokens.div_ceil(isl);
851 let scale2 = isl.div_ceil(ctx_tokens) as f64;
852 let attn = run_context_ops_with(
853 &self.context_ops,
854 &self.db,
855 batch2,
856 isl - prefix,
857 prefix,
858 seq_imbalance_correction_scale,
859 ContextOpFilter::OnlyContextAttention,
860 |op, r| on_op(MixedPass::ContextAttention, op, r),
866 )?;
867 context_attention = attn / scale2;
868 }
869
870 let mut decode_attention = 0.0_f64;
872 if gen_tokens > 0 {
873 let bs = gen_tokens.saturating_mul(self.nextn.saturating_add(1));
874 let s = isl + osl / 2 + 1;
877 decode_attention = run_generation_ops_step_beamed_with(
878 &self.generation_ops,
879 &self.db,
880 bs,
881 1,
882 s,
883 gen_seq_imbalance_correction_scale,
884 true,
885 |op, r| on_op(MixedPass::DecodeAttention, op, r),
886 )?;
887 }
888
889 Ok([
890 shared_non_attention + context_attention + decode_attention,
891 shared_non_attention,
892 context_attention,
893 decode_attention,
894 ])
895 }
896
897 pub fn decode_step_latency(
904 &self,
905 gen_tokens: u32,
906 isl: u32,
907 osl: u32,
908 gen_seq_imbalance_correction_scale: f64,
909 ) -> Result<f64, AicError> {
910 if gen_tokens == 0 {
911 return Ok(0.0);
912 }
913 if self.fpm_ops().is_some() {
919 let rt = RuntimeConfig {
920 batch_size: gen_tokens,
921 isl: isl.saturating_add(osl / 2),
922 osl: 2,
923 ..Default::default()
924 };
925 return self.run_generation_phase(&rt, DEFAULT_STATIC_STRIDE);
926 }
927 let effective_batch = gen_tokens.saturating_mul(self.nextn.saturating_add(1));
928 let s = isl.max(1).saturating_add(osl.max(1) / 2).saturating_add(1);
929 run_generation_ops_step(
930 &self.generation_ops,
931 &self.db,
932 effective_batch,
933 s,
934 gen_seq_imbalance_correction_scale,
935 false,
936 )
937 }
938
939 fn fpm_mixed_step_components(
949 &self,
950 prefill_op: &FpmForwardOp,
951 decode_op: &FpmForwardOp,
952 ctx_tokens: u32,
953 gen_tokens: u32,
954 isl: u32,
955 osl: u32,
956 prefix: u32,
957 ) -> Result<(f64, f64), AicError> {
958 let mut prefill_component = 0.0_f64;
959 if ctx_tokens > 0 {
960 let new_tokens = isl.saturating_sub(prefix);
961 if new_tokens == 0 {
962 return Err(AicError::PerfDatabase(format!(
963 "isl must be greater than prefix, got isl={isl} prefix={prefix}"
964 )));
965 }
966 if ctx_tokens >= new_tokens {
967 let batch = ctx_tokens.div_ceil(new_tokens);
970 prefill_component = prefill_op
971 .query_totals(
972 &self.db,
973 &[
974 batch as f64,
975 (ctx_tokens + gen_tokens) as f64,
976 (batch * prefix) as f64,
977 ],
978 )?
979 .latency_ms;
980 } else {
981 let mut total = 0.0_f64;
983 let mut chunks = 0u32;
984 let mut done = 0u32;
985 while done < new_tokens {
986 let chunk = ctx_tokens.min(new_tokens - done);
987 total += prefill_op
988 .query_totals(
989 &self.db,
990 &[1.0, (chunk + gen_tokens) as f64, (prefix + done) as f64],
991 )?
992 .latency_ms;
993 done += chunk;
994 chunks += 1;
995 }
996 prefill_component = total / chunks as f64;
997 }
998 }
999 let mut marginal_decode = 0.0_f64;
1000 if gen_tokens > 0 {
1001 let rt = RuntimeConfig {
1002 batch_size: gen_tokens,
1003 isl: isl.saturating_add(osl / 2),
1004 osl: 2,
1005 ..Default::default()
1006 };
1007 let gen_ms = self.run_generation_phase(&rt, DEFAULT_STATIC_STRIDE)?;
1008 let baseline_ms = if ctx_tokens > 0 {
1009 let baseline_batch = gen_tokens.saturating_mul(self.nextn.saturating_add(1));
1016 let baseline_kv = baseline_batch as f64 * (rt.isl as f64 + 1.0);
1017 decode_op
1018 .query_pass_baseline(&self.db, baseline_batch, baseline_kv)?
1019 .latency_ms
1020 } else {
1021 0.0
1022 };
1023 marginal_decode = (gen_ms - baseline_ms).max(0.0);
1024 }
1025 Ok((prefill_component, marginal_decode))
1026 }
1027
1028 pub fn run_static_per_op(
1034 &self,
1035 runtime: &RuntimeConfig,
1036 mode: StaticMode,
1037 stride: u32,
1038 ) -> Result<(Vec<PerOpValue>, Vec<PerOpValue>), AicError> {
1039 let (context, generation) = self.run_static_per_op_impl(runtime, mode, stride)?;
1040 Ok((
1041 strip_per_op_metadata(context),
1042 strip_per_op_metadata(generation),
1043 ))
1044 }
1045
1046 pub(crate) fn run_static_per_op_with_metadata(
1050 &self,
1051 runtime: &RuntimeConfig,
1052 mode: StaticMode,
1053 stride: u32,
1054 ) -> Result<(Vec<PerOpValueWithMetadata>, Vec<PerOpValueWithMetadata>), AicError> {
1055 self.run_static_per_op_impl(runtime, mode, stride)
1056 }
1057
1058 fn run_static_per_op_impl(
1059 &self,
1060 runtime: &RuntimeConfig,
1061 mode: StaticMode,
1062 stride: u32,
1063 ) -> Result<(Vec<PerOpValueWithMetadata>, Vec<PerOpValueWithMetadata>), AicError> {
1064 let mut context = PerOpFold::new("context");
1065 if matches!(mode, StaticMode::Context | StaticMode::Both) {
1066 if runtime.prefix >= runtime.isl {
1067 return Err(AicError::InvalidEngineConfig(format!(
1068 "isl must be greater than 0 after removing prefix, but got {}",
1069 runtime.isl as i64 - runtime.prefix as i64
1070 )));
1071 }
1072 run_context_ops_with(
1073 &self.context_ops,
1074 &self.db,
1075 runtime.batch_size,
1076 runtime.isl - runtime.prefix,
1077 runtime.prefix,
1078 runtime.seq_imbalance_correction_scale,
1079 ContextOpFilter::All,
1080 |op, r| context.add(op, r),
1081 )?;
1082 }
1083 let mut generation = PerOpFold::new("generation");
1084 if matches!(mode, StaticMode::Generation | StaticMode::Both) {
1085 self.run_generation_phase_with(runtime, stride, |op, r| generation.add(op, r))?;
1086 }
1087 Ok((context.into_values(), generation.into_values()))
1088 }
1089
1090 #[allow(clippy::too_many_arguments)]
1095 pub fn mixed_step_breakdown_per_op(
1096 &self,
1097 ctx_tokens: u32,
1098 gen_tokens: u32,
1099 isl: u32,
1100 osl: u32,
1101 prefix: u32,
1102 seq_imbalance_correction_scale: f64,
1103 gen_seq_imbalance_correction_scale: f64,
1104 ) -> Result<(Vec<PerOpValue>, Vec<PerOpValue>, Vec<PerOpValue>), AicError> {
1105 let (shared, context_attention, decode_attention) = self.mixed_step_breakdown_per_op_impl(
1106 ctx_tokens,
1107 gen_tokens,
1108 isl,
1109 osl,
1110 prefix,
1111 seq_imbalance_correction_scale,
1112 gen_seq_imbalance_correction_scale,
1113 )?;
1114 Ok((
1115 strip_per_op_metadata(shared),
1116 strip_per_op_metadata(context_attention),
1117 strip_per_op_metadata(decode_attention),
1118 ))
1119 }
1120
1121 #[allow(clippy::too_many_arguments)]
1124 pub(crate) fn mixed_step_breakdown_per_op_with_metadata(
1125 &self,
1126 ctx_tokens: u32,
1127 gen_tokens: u32,
1128 isl: u32,
1129 osl: u32,
1130 prefix: u32,
1131 seq_imbalance_correction_scale: f64,
1132 gen_seq_imbalance_correction_scale: f64,
1133 ) -> Result<MixedStepPerOpValuesWithMetadata, AicError> {
1134 self.mixed_step_breakdown_per_op_impl(
1135 ctx_tokens,
1136 gen_tokens,
1137 isl,
1138 osl,
1139 prefix,
1140 seq_imbalance_correction_scale,
1141 gen_seq_imbalance_correction_scale,
1142 )
1143 }
1144
1145 #[allow(clippy::too_many_arguments)]
1146 fn mixed_step_breakdown_per_op_impl(
1147 &self,
1148 ctx_tokens: u32,
1149 gen_tokens: u32,
1150 isl: u32,
1151 osl: u32,
1152 prefix: u32,
1153 seq_imbalance_correction_scale: f64,
1154 gen_seq_imbalance_correction_scale: f64,
1155 ) -> Result<MixedStepPerOpValuesWithMetadata, AicError> {
1156 if let Some((prefill_op, decode_op)) = self.fpm_ops() {
1163 let (prefill_ms, marginal_decode_ms) = self.fpm_mixed_step_components(
1164 prefill_op,
1165 decode_op,
1166 ctx_tokens,
1167 gen_tokens,
1168 isl.max(1),
1169 osl.max(1),
1170 prefix,
1171 )?;
1172 let mut shared: Vec<PerOpValueWithMetadata> = Vec::new();
1173 if ctx_tokens > 0 {
1174 shared.push((prefill_op.name.clone(), prefill_ms, 0.0, "silicon", None));
1175 }
1176 let mut dec_attn: Vec<PerOpValueWithMetadata> = Vec::new();
1177 if gen_tokens > 0 {
1178 dec_attn.push((
1179 decode_op.name.clone(),
1180 marginal_decode_ms,
1181 0.0,
1182 "silicon",
1183 None,
1184 ));
1185 }
1186 return Ok((shared, Vec::new(), dec_attn));
1187 }
1188 let mut shared = PerOpFold::new("context");
1189 let mut ctx_attn = PerOpFold::new("context");
1190 let mut dec_attn = PerOpFold::new("generation");
1191 self.mixed_step_breakdown_with(
1192 ctx_tokens,
1193 gen_tokens,
1194 isl,
1195 osl,
1196 prefix,
1197 seq_imbalance_correction_scale,
1198 gen_seq_imbalance_correction_scale,
1199 |pass, op, r| {
1200 let out = match pass {
1201 MixedPass::SharedNonAttention => &mut shared,
1202 MixedPass::ContextAttention => &mut ctx_attn,
1203 MixedPass::DecodeAttention => &mut dec_attn,
1204 };
1205 out.add(op, r);
1206 },
1207 )?;
1208 let mut ctx_attn = ctx_attn.into_values();
1209 if ctx_tokens > 0 {
1210 let scale2 = isl.max(1).div_ceil(ctx_tokens) as f64;
1214 for entry in &mut ctx_attn {
1215 entry.1 /= scale2;
1216 entry.2 /= scale2;
1217 }
1218 }
1219 Ok((shared.into_values(), ctx_attn, dec_attn.into_values()))
1220 }
1221
1222 pub fn decode_step_per_op(
1224 &self,
1225 gen_tokens: u32,
1226 isl: u32,
1227 osl: u32,
1228 gen_seq_imbalance_correction_scale: f64,
1229 ) -> Result<Vec<PerOpValue>, AicError> {
1230 self.decode_step_per_op_impl(gen_tokens, isl, osl, gen_seq_imbalance_correction_scale)
1231 .map(strip_per_op_metadata)
1232 }
1233
1234 pub(crate) fn decode_step_per_op_with_metadata(
1237 &self,
1238 gen_tokens: u32,
1239 isl: u32,
1240 osl: u32,
1241 gen_seq_imbalance_correction_scale: f64,
1242 ) -> Result<Vec<PerOpValueWithMetadata>, AicError> {
1243 self.decode_step_per_op_impl(gen_tokens, isl, osl, gen_seq_imbalance_correction_scale)
1244 }
1245
1246 fn decode_step_per_op_impl(
1247 &self,
1248 gen_tokens: u32,
1249 isl: u32,
1250 osl: u32,
1251 gen_seq_imbalance_correction_scale: f64,
1252 ) -> Result<Vec<PerOpValueWithMetadata>, AicError> {
1253 let mut out = PerOpFold::new("generation");
1254 if gen_tokens == 0 {
1255 return Ok(out.into_values());
1256 }
1257 let effective_batch = gen_tokens.saturating_mul(self.nextn.saturating_add(1));
1258 let s = isl.max(1).saturating_add(osl.max(1) / 2).saturating_add(1);
1259 run_generation_ops_step_beamed_with(
1260 &self.generation_ops,
1261 &self.db,
1262 effective_batch,
1263 1,
1264 s,
1265 gen_seq_imbalance_correction_scale,
1266 false,
1267 |op, r| out.add(op, r),
1268 )?;
1269 Ok(out.into_values())
1270 }
1271
1272 #[allow(clippy::too_many_arguments)]
1277 pub fn evaluate_context_ops(
1278 &self,
1279 indices: &[usize],
1280 batch_size: u32,
1281 s: u32,
1282 prefix: u32,
1283 seq_imbalance_correction_scale: f64,
1284 x_override: Option<u32>,
1285 ) -> Result<Vec<PerOpValue>, AicError> {
1286 let mut out = PerOpFold::new("context");
1287 for &i in indices {
1288 let op = self.context_ops.get(i).ok_or_else(|| {
1289 AicError::InvalidEngineConfig(format!(
1290 "evaluate_context_ops: index {i} out of range ({} context ops)",
1291 self.context_ops.len()
1292 ))
1293 })?;
1294 let r = query_context_op(
1295 op,
1296 &self.db,
1297 batch_size,
1298 s,
1299 prefix,
1300 seq_imbalance_correction_scale,
1301 x_override,
1302 )?;
1303 out.add(op, r);
1304 }
1305 Ok(strip_per_op_metadata(out.into_values()))
1306 }
1307
1308 #[allow(clippy::too_many_arguments)]
1311 pub fn evaluate_generation_ops(
1312 &self,
1313 indices: &[usize],
1314 batch_size: u32,
1315 s: u32,
1316 gen_seq_imbalance_correction_scale: f64,
1317 prefix: u32,
1318 x_override: Option<u32>,
1319 ) -> Result<Vec<PerOpValue>, AicError> {
1320 let mut out = PerOpFold::new("generation");
1321 for &i in indices {
1322 let op = self.generation_ops.get(i).ok_or_else(|| {
1323 AicError::InvalidEngineConfig(format!(
1324 "evaluate_generation_ops: index {i} out of range ({} generation ops)",
1325 self.generation_ops.len()
1326 ))
1327 })?;
1328 let r = query_generation_op(
1329 op,
1330 &self.db,
1331 batch_size,
1332 1,
1333 s,
1334 gen_seq_imbalance_correction_scale,
1335 prefix,
1336 x_override,
1337 )?;
1338 out.add(op, r);
1339 }
1340 Ok(strip_per_op_metadata(out.into_values()))
1341 }
1342
1343 #[allow(clippy::too_many_arguments)]
1348 pub fn evaluate_ops_json(
1349 &self,
1350 ops_json: &str,
1351 is_context: bool,
1352 batch_size: u32,
1353 s: u32,
1354 prefix: u32,
1355 imbalance_correction_scale: f64,
1356 x_override: Option<u32>,
1357 ) -> Result<Vec<PerOpValue>, AicError> {
1358 let ops: Vec<Op> = serde_json::from_str(ops_json).map_err(|e| {
1359 AicError::InvalidEngineConfig(format!("evaluate_ops_json: invalid op list JSON: {e}"))
1360 })?;
1361 let mut out = PerOpFold::new(if is_context { "context" } else { "generation" });
1362 for op in &ops {
1363 let r = if is_context {
1364 query_context_op(
1365 op,
1366 &self.db,
1367 batch_size,
1368 s,
1369 prefix,
1370 imbalance_correction_scale,
1371 x_override,
1372 )?
1373 } else {
1374 query_generation_op(
1375 op,
1376 &self.db,
1377 batch_size,
1378 1,
1379 s,
1380 imbalance_correction_scale,
1381 prefix,
1382 x_override,
1383 )?
1384 };
1385 out.add(op, r);
1386 }
1387 Ok(strip_per_op_metadata(out.into_values()))
1388 }
1389
1390 #[allow(clippy::too_many_arguments)]
1398 pub fn evaluate_ops_sol_json(
1399 &self,
1400 ops_json: &str,
1401 is_context: bool,
1402 batch_size: u32,
1403 s: u32,
1404 prefix: u32,
1405 imbalance_correction_scale: f64,
1406 x_override: Option<u32>,
1407 ) -> Result<Vec<PerOpSolValue>, AicError> {
1408 let ops: Vec<Op> = serde_json::from_str(ops_json).map_err(|e| {
1409 AicError::InvalidEngineConfig(format!(
1410 "evaluate_ops_sol_json: invalid op list JSON: {e}"
1411 ))
1412 })?;
1413 let sol_db = self.db.sol_full_view();
1414 let mut out = PerOpSolFold::default();
1415 for op in &ops {
1416 let r = if is_context {
1417 query_context_op(
1418 op,
1419 &sol_db,
1420 batch_size,
1421 s,
1422 prefix,
1423 imbalance_correction_scale,
1424 x_override,
1425 )?
1426 } else {
1427 query_generation_op(
1428 op,
1429 &sol_db,
1430 batch_size,
1431 1,
1432 s,
1433 imbalance_correction_scale,
1434 prefix,
1435 x_override,
1436 )?
1437 };
1438 out.add(op, r)?;
1439 }
1440 Ok(out.into_values())
1441 }
1442
1443 pub fn forward_pass_time_ms(
1459 &self,
1460 metrics_by_rank: &[ForwardPassMetrics],
1461 ) -> Result<f64, AicError> {
1462 if metrics_by_rank.is_empty() {
1463 return Err(AicError::InvalidForwardPassMetrics(
1464 "at least one attention-DP rank metric required".to_string(),
1465 ));
1466 }
1467 for metrics in metrics_by_rank {
1468 validate_forward_pass_metrics(metrics)?;
1469 }
1470 let mut max_latency = 0.0_f64;
1471 for metrics in metrics_by_rank {
1472 let rank_latency = self.rank_latency_ms(metrics)?;
1473 if rank_latency > max_latency {
1474 max_latency = rank_latency;
1475 }
1476 }
1477 Ok(max_latency)
1478 }
1479
1480 fn rank_latency_ms(&self, metrics: &ForwardPassMetrics) -> Result<f64, AicError> {
1487 let sched = &metrics.scheduled_requests;
1488 let has_prefill = sched.sum_prefill_tokens > 0;
1495 let has_decode = sched.num_decode_requests > 0 || sched.sum_decode_kv_tokens > 0;
1496
1497 if let Some((prefill_op, decode_op)) = self.fpm_ops() {
1505 let mut total = 0.0_f64;
1511 if has_prefill {
1512 total += prefill_op
1513 .query_totals(
1514 &self.db,
1515 &[
1516 sched.num_prefill_requests as f64,
1517 sched.sum_prefill_tokens as f64,
1518 sched.sum_prefill_kv_tokens as f64,
1519 ],
1520 )?
1521 .latency_ms;
1522 }
1523 if has_decode {
1524 let decode_ms = decode_op
1525 .query_totals(
1526 &self.db,
1527 &[
1528 sched.num_decode_requests as f64,
1529 sched.sum_decode_kv_tokens as f64,
1530 ],
1531 )?
1532 .latency_ms;
1533 if has_prefill {
1534 let baseline_ms = decode_op
1538 .query_pass_baseline(
1539 &self.db,
1540 sched.num_decode_requests,
1541 sched.sum_decode_kv_tokens as f64,
1542 )?
1543 .latency_ms;
1544 total += (decode_ms - baseline_ms).max(0.0);
1545 } else {
1546 total += decode_ms;
1547 }
1548 }
1549 return Ok(total);
1550 }
1551
1552 if has_prefill && has_decode {
1553 let n_prefill = sched.num_prefill_requests.max(1);
1558 let new_tokens_per_req = sched.sum_prefill_tokens / n_prefill;
1559 let prefix_per_req = sched.sum_prefill_kv_tokens / n_prefill;
1560 let n_decode = sched.num_decode_requests.max(1);
1561 let kv_per_req = sched.sum_decode_kv_tokens / n_decode;
1562 let ctx_tokens = sched.sum_prefill_tokens;
1563 let gen_tokens = sched.num_decode_requests;
1564 return get_mix_step_ops(
1565 &self.context_ops,
1566 &self.generation_ops,
1567 &self.db,
1568 ctx_tokens,
1569 gen_tokens,
1570 new_tokens_per_req.max(1),
1571 prefix_per_req,
1572 sched.sum_prefill_kv_tokens,
1573 kv_per_req,
1574 n_decode,
1575 );
1576 }
1577
1578 let mut total = 0.0_f64;
1579
1580 if has_prefill {
1581 let n_prefill = sched.num_prefill_requests.max(1);
1582 let new_tokens_per_req = sched.sum_prefill_tokens / n_prefill;
1583 let prefix_per_req = sched.sum_prefill_kv_tokens / n_prefill;
1584 total += run_context_ops(
1585 &self.context_ops,
1586 &self.db,
1587 n_prefill,
1588 new_tokens_per_req,
1589 prefix_per_req,
1590 1.0,
1591 ContextOpFilter::All,
1592 )?;
1593 }
1594
1595 if has_decode {
1596 let n_decode = sched.num_decode_requests.max(1);
1597 let kv_per_req = sched.sum_decode_kv_tokens / n_decode;
1598 total += run_generation_ops_step(
1599 &self.generation_ops,
1600 &self.db,
1601 n_decode,
1602 kv_per_req,
1603 1.0,
1604 false,
1605 )?;
1606 }
1607
1608 Ok(total)
1609 }
1610}
1611
1612#[cfg(test)]
1613mod tests {
1614 use super::*;
1615 use std::collections::BTreeMap;
1616 use std::path::PathBuf;
1617
1618 use crate::common::enums::{FmhaQuantMode, GemmQuantMode, KvCacheQuantMode};
1619 use crate::operators::op::Op;
1620 use crate::operators::{
1621 ContextAttentionOp, ElementwiseOp, GemmOp, GenerationAttentionOp, MoeAllToAllOp,
1622 };
1623 use crate::perfmodel::EngineConfig;
1624 use crate::perfmodel::engine::spec::EngineSpec;
1625 use crate::{BackendKind, ParallelMapping, QuantizationConfig};
1626
1627 fn systems_root() -> PathBuf {
1628 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1629 .join("../../python/aisimulate/src/aiconfigurator_core/systems")
1630 }
1631
1632 const TEST_MODEL: &str = "MiniMaxAI/MiniMax-M2.5";
1633
1634 fn context_ops() -> Vec<Op> {
1639 vec![
1640 Op::Elementwise(ElementwiseOp {
1641 name: "rmsnorm".into(),
1642 scale_factor: 1.0,
1643 bytes_per_token: 8192.0,
1644 scale_num_tokens: 1,
1645 seq_split: 1,
1646 }),
1647 Op::Gemm(GemmOp {
1648 name: "qkv_gemm".into(),
1649 scale_factor: 1.0,
1650 n: 4096,
1651 k: 4096,
1652 quant_mode: GemmQuantMode::Fp8Block,
1653 scale_num_tokens: 0,
1654 low_precision_input: false,
1655 seq_split: 1,
1656 below_grid_sol: false,
1657 }),
1658 Op::ContextAttention(ContextAttentionOp {
1659 name: "context_attention".into(),
1660 scale_factor: 1.0,
1661 n: 32,
1662 n_kv: 8,
1663 head_size: 128,
1664 window_size: 0,
1665 kv_cache_dtype: KvCacheQuantMode::Fp8,
1666 fmha_quant_mode: FmhaQuantMode::Bfloat16,
1667 use_qk_norm: false,
1668 cp_size: 1,
1669 lane_order: crate::operators::attention::b200_vllm_context_lane_order(),
1670 }),
1671 ]
1672 }
1673
1674 fn generation_ops() -> Vec<Op> {
1675 vec![
1676 Op::Elementwise(ElementwiseOp {
1677 name: "rmsnorm".into(),
1678 scale_factor: 1.0,
1679 bytes_per_token: 8192.0,
1680 scale_num_tokens: 1,
1681 seq_split: 1,
1682 }),
1683 Op::GenerationAttention(GenerationAttentionOp {
1684 name: "generation_attention".into(),
1685 scale_factor: 1.0,
1686 n: 32,
1687 n_kv: 8,
1688 head_size: 128,
1689 window_size: 0,
1690 kv_cache_dtype: KvCacheQuantMode::Fp8,
1691 lane_order: crate::operators::attention::b200_vllm_generation_lane_order(),
1692 }),
1693 ]
1694 }
1695
1696 fn fixture_engine_config(nextn: Option<u32>) -> EngineConfig {
1697 EngineConfig {
1698 schema_version: crate::ENGINE_CONFIG_SCHEMA_VERSION,
1699 model_name: TEST_MODEL.to_string(),
1700 system_name: "b200_sxm".to_string(),
1701 systems_path: None,
1702 backend: BackendKind::Vllm,
1703 backend_version: Some("0.24.0".to_string()),
1704 forward_model: None,
1705 kv_block_size: None,
1706 parallel: ParallelMapping {
1707 tp_size: 8,
1708 pp_size: 1,
1709 attention_dp_size: Some(1),
1710 moe_tp_size: Some(1),
1711 moe_ep_size: Some(8),
1712 cp_size: None,
1713 },
1714 quantization: QuantizationConfig {
1715 weight_dtype: None,
1716 moe_dtype: None,
1717 activation_dtype: None,
1718 kv_cache_dtype: None,
1719 },
1720 speculative: nextn.map(|n| crate::SpeculativeConfig { nextn: Some(n) }),
1721 enable_shared_layer: None,
1722 strict_provenance: false,
1723 tolerate_dirless_version: false,
1724 database_mode: Default::default(),
1725 transfer_policy: None,
1726 extra: BTreeMap::new(),
1727 }
1728 }
1729
1730 fn build_engine(nextn: Option<u32>) -> Engine {
1732 let db = PerfDatabase::load(&systems_root(), "b200_sxm", "vllm", "0.24.0").unwrap();
1733 let spec = EngineSpec::new(
1734 fixture_engine_config(nextn),
1735 context_ops(),
1736 generation_ops(),
1737 );
1738 Engine::build(spec, Arc::new(db)).unwrap()
1739 }
1740
1741 fn runtime(batch_size: u32, isl: u32, osl: u32) -> RuntimeConfig {
1742 RuntimeConfig {
1743 batch_size,
1744 isl,
1745 osl,
1746 ..Default::default()
1747 }
1748 }
1749
1750 #[test]
1751 fn per_op_fold_attaches_the_inference_phase_only_to_executed_fallbacks() {
1752 use crate::operators::base::{MoeCommFallback, Source};
1753
1754 let op = context_ops().remove(0);
1755 let fallback = MoeCommFallback {
1756 comm_backend: "deepep_ht",
1757 requested_ep_size: 32,
1758 requested_node_num: 8,
1759 measurement_ep_size: 8,
1760 measurement_node_num: 1,
1761 };
1762 for inference_phase in ["context", "generation"] {
1763 let mut fold = PerOpFold::new(inference_phase);
1764 fold.add(
1765 &op,
1766 PerformanceResult::new(1.0, Source::Estimated).with_moe_comm_fallback(fallback),
1767 );
1768 assert_eq!(
1769 fold.into_values()[0].4,
1770 Some(((inference_phase, "deepep_ht", 32, 8, 8, 1), vec![]))
1771 );
1772 }
1773
1774 let mut repeated_name = PerOpFold::new("context");
1775 repeated_name.add(
1776 &op,
1777 PerformanceResult::new(1.0, Source::Estimated).with_moe_comm_fallback(fallback),
1778 );
1779 repeated_name.add(
1780 &op,
1781 PerformanceResult::new(1.0, Source::Estimated).with_moe_comm_fallback(
1782 MoeCommFallback {
1783 comm_backend: "deepep_ll",
1784 ..fallback
1785 },
1786 ),
1787 );
1788 assert_eq!(
1789 repeated_name.into_values()[0].4,
1790 Some((
1791 ("context", "deepep_ht", 32, 8, 8, 1),
1792 vec![("context", "deepep_ll", 32, 8, 8, 1)],
1793 ))
1794 );
1795
1796 let mut exact = PerOpFold::new("context");
1797 exact.add(&op, PerformanceResult::new(1.0, Source::Silicon));
1798 assert_eq!(exact.into_values()[0].4, None);
1799 }
1800
1801 #[test]
1802 fn per_op_fold_allocates_additional_storage_only_for_distinct_fallbacks_after_the_first() {
1803 use crate::operators::base::{MoeCommFallback, Source};
1804
1805 let op = context_ops().remove(0);
1806 let ht = MoeCommFallback {
1807 comm_backend: "deepep_ht",
1808 requested_ep_size: 32,
1809 requested_node_num: 8,
1810 measurement_ep_size: 8,
1811 measurement_node_num: 1,
1812 };
1813 let ll = MoeCommFallback {
1814 comm_backend: "deepep_ll",
1815 ..ht
1816 };
1817
1818 let mut empty = PerOpFold::new("context");
1819 empty.add(&op, PerformanceResult::new(1.0, Source::Silicon));
1820 assert!(empty.into_values().pop().unwrap().4.is_none());
1821
1822 let mut single = PerOpFold::new("context");
1823 single.add(
1824 &op,
1825 PerformanceResult::new(1.0, Source::Estimated).with_moe_comm_fallback(ht),
1826 );
1827 let (first, additional) = single.into_values().pop().unwrap().4.unwrap();
1828 assert_eq!(first, ("context", "deepep_ht", 32, 8, 8, 1));
1829 assert_eq!(additional.capacity(), 0);
1830
1831 let mut multiple = PerOpFold::new("generation");
1832 for fallback in [ht, ht, ll, ll] {
1833 multiple.add(
1834 &op,
1835 PerformanceResult::new(1.0, Source::Estimated).with_moe_comm_fallback(fallback),
1836 );
1837 }
1838 let (first, additional) = multiple.into_values().pop().unwrap().4.unwrap();
1839 assert_eq!(first, ("generation", "deepep_ht", 32, 8, 8, 1));
1840 assert_eq!(additional, vec![("generation", "deepep_ll", 32, 8, 8, 1)]);
1841 }
1842
1843 #[test]
1844 fn generation_step_preserves_distinct_same_name_deepep_fallbacks() {
1845 let mut config = fixture_engine_config(None);
1846 config.system_name = "gb200".to_string();
1847 config.backend = BackendKind::Sglang;
1848 config.backend_version = Some("0.5.16".to_string());
1849
1850 let a2a = |moe_ep_size, node_num| {
1851 Op::MoeAllToAll(MoeAllToAllOp {
1852 name: "generation_moe_dispatch".to_string(),
1853 scale_factor: 1.0,
1854 phase: "dispatch".to_string(),
1855 comm_backend: "deepep_ll".to_string(),
1856 comm_dtype: "default".to_string(),
1857 hidden_size: 7168,
1858 topk: 8,
1859 num_experts: 256,
1860 moe_ep_size,
1861 node_num,
1862 sms: 0,
1863 attention_tp_size: 1,
1864 })
1865 };
1866 let spec = EngineSpec::new(config, Vec::new(), vec![a2a(32, 8), a2a(64, 16)]);
1867 let engine = Engine::from_spec_bytes(&spec.to_bincode().unwrap(), &systems_root())
1868 .expect("shipped GB200 SGLang DeepEP data must load");
1869 let runtime = RuntimeConfig {
1870 batch_size: 1,
1871 isl: 1024,
1872 osl: 2,
1873 ..Default::default()
1874 };
1875
1876 let (_, generation) = engine
1877 .run_static_per_op_with_metadata(&runtime, StaticMode::Generation, 32)
1878 .unwrap();
1879 assert_eq!(generation.len(), 1, "same-name ops must remain name-folded");
1880 assert_eq!(
1881 generation[0].4,
1882 Some((
1883 ("generation", "deepep_ll", 32, 8, 8, 1),
1884 vec![("generation", "deepep_ll", 64, 16, 8, 1)],
1885 ))
1886 );
1887 }
1888
1889 #[test]
1890 fn from_spec_bytes_shares_parsed_tables_across_engines() {
1891 use crate::operators::util_empirical::ProvenanceTier;
1892
1893 let spec1 = EngineSpec::new(fixture_engine_config(None), context_ops(), generation_ops());
1896 let spec2 = EngineSpec::new(
1897 fixture_engine_config(Some(1)),
1898 context_ops(),
1899 generation_ops(),
1900 );
1901 let e1 = Engine::from_spec_bytes(&spec1.to_bincode().unwrap(), &systems_root()).unwrap();
1902 let e2 = Engine::from_spec_bytes(&spec2.to_bincode().unwrap(), &systems_root()).unwrap();
1903 assert!(
1904 std::sync::Arc::ptr_eq(e1.database().tables_arc(), e2.database().tables_arc()),
1905 "engines over the same db identity must share parsed tables"
1906 );
1907 e1.database().note_provenance(ProvenanceTier::Empirical);
1910 assert_eq!(e2.database().worst_provenance(), ProvenanceTier::Silicon);
1911 }
1912
1913 #[test]
1914 fn both_equals_context_plus_generation() {
1915 let engine = build_engine(None);
1916 let rt = runtime(1, 1024, 8);
1917 let both = engine.run_static(&rt, StaticMode::Both, 32).unwrap();
1918 let ctx = engine.run_static(&rt, StaticMode::Context, 32).unwrap();
1919 let generation = engine.run_static(&rt, StaticMode::Generation, 32).unwrap();
1920
1921 assert!((both.context_ms - ctx.context_ms).abs() < 1e-9);
1922 assert!((both.generation_ms - generation.generation_ms).abs() < 1e-9);
1923 assert!((both.total_ms - (ctx.context_ms + generation.generation_ms)).abs() < 1e-9);
1924 assert!((both.total_ms - (ctx.total_ms + generation.total_ms)).abs() < 1e-9);
1926 }
1927
1928 #[test]
1929 fn context_mode_has_zero_generation() {
1930 let engine = build_engine(None);
1931 let rt = runtime(1, 1024, 8);
1932 let ctx = engine.run_static(&rt, StaticMode::Context, 32).unwrap();
1933 assert!(ctx.context_ms > 0.0, "context latency must be non-trivial");
1934 assert_eq!(ctx.generation_ms, 0.0);
1935 assert_eq!(ctx.total_ms, ctx.context_ms);
1936 }
1937
1938 #[test]
1939 fn generation_mode_has_zero_context() {
1940 let engine = build_engine(None);
1941 let rt = runtime(1, 1024, 8);
1942 let generation = engine.run_static(&rt, StaticMode::Generation, 32).unwrap();
1943 assert!(
1944 generation.generation_ms > 0.0,
1945 "generation latency must be non-trivial"
1946 );
1947 assert_eq!(generation.context_ms, 0.0);
1948 assert_eq!(generation.total_ms, generation.generation_ms);
1949 }
1950
1951 #[test]
1952 fn stride_honored() {
1953 let engine = build_engine(None);
1954 let rt = runtime(1, 1024, 9);
1959 let fine = engine.run_static(&rt, StaticMode::Generation, 1).unwrap();
1960 let coarse = engine.run_static(&rt, StaticMode::Generation, 32).unwrap();
1961 assert!(fine.generation_ms > 0.0 && coarse.generation_ms > 0.0);
1962 assert!(
1963 (fine.generation_ms - coarse.generation_ms).abs() > 1e-9,
1964 "stride=1 ({}) and stride=32 ({}) must differ for osl=9",
1965 fine.generation_ms,
1966 coarse.generation_ms
1967 );
1968
1969 let one_step = run_generation_ops_step(
1972 &engine.generation_ops,
1973 engine.database(),
1974 1, 1024 + 0 + 1,
1976 1.0,
1977 false,
1978 )
1979 .unwrap();
1980 assert!((coarse.generation_ms - one_step * 8.0).abs() < 1e-6);
1981 }
1982
1983 #[test]
1984 fn osl_one_yields_zero_generation() {
1985 let engine = build_engine(None);
1986 let rt = runtime(1, 1024, 1);
1987 let generation = engine.run_static(&rt, StaticMode::Generation, 32).unwrap();
1988 assert_eq!(generation.generation_ms, 0.0);
1989 }
1990
1991 #[test]
1992 fn prefix_ge_isl_errors() {
1993 let engine = build_engine(None);
1994 let rt = RuntimeConfig {
1995 batch_size: 1,
1996 isl: 512,
1997 osl: 2,
1998 prefix: 512,
1999 ..Default::default()
2000 };
2001 assert!(engine.run_static(&rt, StaticMode::Context, 32).is_err());
2002 }
2003
2004 #[test]
2005 fn mixed_step_empty_is_zero() {
2006 let engine = build_engine(None);
2007 assert_eq!(
2008 engine
2009 .mixed_step_latency(0, 0, 1024, 8, 0, 1.0, 1.0)
2010 .unwrap(),
2011 0.0
2012 );
2013 }
2014
2015 #[test]
2016 fn mixed_step_nonempty_is_positive() {
2017 let engine = build_engine(None);
2022 let ms = engine
2023 .mixed_step_latency(1024, 2, 1024, 8, 0, 1.0, 1.0)
2024 .unwrap();
2025 assert!(
2026 ms > 0.0 && ms.is_finite(),
2027 "mixed-step latency must be > 0, got {ms}"
2028 );
2029 let breakdown = engine
2030 .mixed_step_breakdown(1024, 2, 1024, 8, 0, 1.0, 1.0)
2031 .unwrap();
2032 assert_eq!(breakdown[0], breakdown[1] + breakdown[2] + breakdown[3]);
2033 assert_eq!(ms, breakdown[0]);
2034 }
2035
2036 fn build_fpm_engine(tmp: &std::path::Path, nextn: Option<u32>) -> Result<Engine, AicError> {
2042 use crate::perf_database::fpm_forward::tests::{
2043 default_identity, default_rows, write_pair,
2044 };
2045 write_pair(tmp, &default_rows());
2046 let mut db = PerfDatabase::load(&systems_root(), "b200_sxm", "vllm", "0.24.0").unwrap();
2047 db.set_fpm_forward_for_test(crate::perf_database::FpmForwardTable::new(
2048 tmp.to_path_buf(),
2049 "b200_sxm",
2050 "vllm",
2051 "0.25.1",
2052 ));
2053 let fpm_op = |phase: FpmPhase| {
2054 Op::FpmForward(FpmForwardOp {
2055 name: format!("fpm_forward_{}", phase.as_str()),
2056 phase,
2057 model_path: "org/model-a".to_string(),
2058 match_identity: default_identity(4),
2059 weight_bytes: 0.0,
2060 sol_ops: vec![],
2061 })
2062 };
2063 let spec = EngineSpec::new(
2064 fixture_engine_config(nextn),
2065 vec![fpm_op(FpmPhase::Prefill)],
2066 vec![fpm_op(FpmPhase::Decode)],
2067 );
2068 Engine::build(spec, Arc::new(db))
2069 }
2070
2071 #[test]
2072 fn fpm_build_rejects_mtp_and_bad_shape() {
2073 let tmp = tempfile::tempdir().unwrap();
2074 let err = build_fpm_engine(tmp.path(), Some(1)).unwrap_err();
2075 assert!(err.to_string().contains("MTP"), "{err}");
2076
2077 use crate::perf_database::fpm_forward::tests::default_identity;
2079 let db = PerfDatabase::load(&systems_root(), "b200_sxm", "vllm", "0.24.0").unwrap();
2080 let fpm_op = Op::FpmForward(FpmForwardOp {
2081 name: "fpm_forward_prefill".into(),
2082 phase: FpmPhase::Prefill,
2083 model_path: "org/model-a".into(),
2084 match_identity: default_identity(4),
2085 weight_bytes: 0.0,
2086 sol_ops: vec![],
2087 });
2088 let spec = EngineSpec::new(
2089 fixture_engine_config(None),
2090 vec![fpm_op, context_ops().remove(0)],
2091 generation_ops(),
2092 );
2093 let err = Engine::build(spec, Arc::new(db)).unwrap_err();
2094 assert!(err.to_string().contains("exactly one FpmForward"), "{err}");
2095 }
2096
2097 #[test]
2102 fn fpm_mixed_step_is_prefill_plus_marginal_decode() {
2103 let tmp = tempfile::tempdir().unwrap();
2104 let engine = build_fpm_engine(tmp.path(), None).unwrap();
2105 let ms = engine
2111 .mixed_step_latency(2048, 8, 2048, 0, 0, 1.0, 1.0)
2112 .unwrap();
2113 let pre = 20.0 + (40.0 - 20.0) * (2056.0 - 2048.0) / (4096.0 - 2048.0);
2114 let w = (16392.0 - 4096.0) / (65536.0 - 4096.0);
2115 let decode = 7.0 + (9.0 - 7.0) * w;
2116 let expected = pre + (decode - 6.0);
2117 assert!((ms - expected).abs() < 1e-9, "got {ms}, want {expected}");
2118 }
2119
2120 fn build_fpm_engine_with_rows(
2123 tmp: &std::path::Path,
2124 rows: &[crate::perf_database::fpm_forward::tests::RowSpec],
2125 ) -> Result<Engine, AicError> {
2126 use crate::perf_database::fpm_forward::tests::{default_identity, write_pair};
2127 write_pair(tmp, rows);
2128 let mut db = PerfDatabase::load(&systems_root(), "b200_sxm", "vllm", "0.24.0").unwrap();
2129 db.set_fpm_forward_for_test(crate::perf_database::FpmForwardTable::new(
2130 tmp.to_path_buf(),
2131 "b200_sxm",
2132 "vllm",
2133 "0.25.1",
2134 ));
2135 let fpm_op = |phase: FpmPhase| {
2136 Op::FpmForward(FpmForwardOp {
2137 name: format!("fpm_forward_{}", phase.as_str()),
2138 phase,
2139 model_path: "org/model-a".to_string(),
2140 match_identity: default_identity(4),
2141 weight_bytes: 0.0,
2142 sol_ops: vec![],
2143 })
2144 };
2145 let spec = EngineSpec::new(
2146 fixture_engine_config(None),
2147 vec![fpm_op(FpmPhase::Prefill)],
2148 vec![fpm_op(FpmPhase::Decode)],
2149 );
2150 Engine::build(spec, Arc::new(db))
2151 }
2152
2153 fn cliff_rows() -> Vec<crate::perf_database::fpm_forward::tests::RowSpec> {
2154 use crate::perf_database::fpm_forward::tests::RowSpec;
2155 let mk = |kind: &'static str, batch: u32, prefill: u32, kv: u32, lat: f64| RowSpec {
2156 workload_kind: kind,
2157 batch_size: batch,
2158 total_prefill_tokens: prefill,
2159 total_kv_read_tokens: kv,
2160 latency_ms: lat,
2161 ..RowSpec::default()
2162 };
2163 vec![
2164 mk("prefill", 1, 2048, 0, 47.0),
2166 mk("prefill", 1, 2049, 0, 99.0),
2167 mk("prefill", 1, 4096, 0, 99.0),
2168 mk("prefill", 1, 1032, 0, 10.0),
2170 mk("prefill", 1, 1032, 1024, 14.0),
2171 mk("decode", 8, 0, 8, 6.0),
2172 mk("decode", 8, 0, 4096, 7.0),
2173 mk("decode", 8, 0, 65536, 9.0),
2174 ]
2175 }
2176
2177 #[test]
2181 fn fpm_mixed_step_total_crosses_the_graph_cliff() {
2182 let tmp = tempfile::tempdir().unwrap();
2183 let engine = build_fpm_engine_with_rows(tmp.path(), &cliff_rows()).unwrap();
2184 let graph = engine
2186 .mixed_step_breakdown(2048, 0, 2048, 0, 0, 1.0, 1.0)
2187 .unwrap();
2188 assert!(
2189 (graph[1] - 47.0).abs() < 1e-9,
2190 "graph-side prefill {}",
2191 graph[1]
2192 );
2193 let eager = engine
2195 .mixed_step_breakdown(2048, 8, 2048, 0, 0, 1.0, 1.0)
2196 .unwrap();
2197 assert!(
2198 (eager[1] - 99.0).abs() < 1e-9,
2199 "eager-side prefill {}",
2200 eager[1]
2201 );
2202 assert!(eager[1] > graph[1] * 2.0 - 1e-9);
2203 }
2204
2205 #[test]
2208 fn fpm_mixed_step_chunks_average_exact_coordinates() {
2209 let tmp = tempfile::tempdir().unwrap();
2210 let engine = build_fpm_engine_with_rows(tmp.path(), &cliff_rows()).unwrap();
2211 let parts = engine
2214 .mixed_step_breakdown(1024, 8, 2048, 0, 0, 1.0, 1.0)
2215 .unwrap();
2216 assert!((parts[1] - 12.0).abs() < 1e-9, "chunk average {}", parts[1]);
2217 }
2218
2219 #[test]
2222 fn fpm_genonly_step_keeps_full_decode() {
2223 let tmp = tempfile::tempdir().unwrap();
2224 let engine = build_fpm_engine(tmp.path(), None).unwrap();
2225 let ms = engine.decode_step_latency(8, 511, 0, 1.0).unwrap();
2228 assert!((ms - 7.0).abs() < 1e-12, "got {ms}");
2229 let mixed = engine
2231 .mixed_step_latency(0, 8, 511, 0, 0, 1.0, 1.0)
2232 .unwrap();
2233 assert!((mixed - 7.0).abs() < 1e-12, "got {mixed}");
2234 assert_eq!(engine.decode_step_latency(0, 511, 0, 1.0).unwrap(), 0.0);
2235 }
2236
2237 #[test]
2243 fn fpm_rank_prefix_cached_payload_is_decode_only() {
2244 use crate::fpm::{ForwardPassMetrics, ScheduledRequestMetrics};
2245 let tmp = tempfile::tempdir().unwrap();
2246 let engine = build_fpm_engine(tmp.path(), None).unwrap();
2247 let metrics = ForwardPassMetrics {
2248 scheduled_requests: ScheduledRequestMetrics {
2249 num_prefill_requests: 1,
2250 sum_prefill_tokens: 0,
2251 sum_prefill_kv_tokens: 4096,
2252 num_decode_requests: 8,
2253 sum_decode_kv_tokens: 4096, ..Default::default()
2255 },
2256 ..Default::default()
2257 };
2258 let ms = engine.forward_pass_time_ms(&[metrics]).unwrap();
2260 assert!((ms - 7.0).abs() < 1e-12, "{ms}");
2261 }
2262
2263 #[test]
2266 fn fpm_rank_latency_marginal_composition() {
2267 use crate::fpm::{ForwardPassMetrics, ScheduledRequestMetrics};
2268 let tmp = tempfile::tempdir().unwrap();
2269 let engine = build_fpm_engine(tmp.path(), None).unwrap();
2270
2271 let mixed = ForwardPassMetrics {
2272 scheduled_requests: ScheduledRequestMetrics {
2273 num_prefill_requests: 2,
2274 sum_prefill_tokens: 2 * 1024,
2275 sum_prefill_kv_tokens: 0,
2276 num_decode_requests: 8,
2277 sum_decode_kv_tokens: 8 * 4096,
2278 ..Default::default()
2279 },
2280 ..Default::default()
2281 };
2282 let w = (32768.0 - 4096.0) / (65536.0 - 4096.0);
2286 let decode = 7.0 + (9.0 - 7.0) * w;
2287 let expected = 21.0 + (decode - 6.0);
2288 let got = engine.forward_pass_time_ms(&[mixed]).unwrap();
2289 assert!((got - expected).abs() < 1e-9, "got {got}, want {expected}");
2290 }
2291
2292 #[test]
2296 fn fpm_rank_mixed_baseline_holds_bracket_curve_floors() {
2297 use crate::fpm::{ForwardPassMetrics, ScheduledRequestMetrics};
2298 use crate::perf_database::fpm_forward::tests::RowSpec;
2299 let mk = |kind: &'static str, batch: u32, prefill: u32, kv: u32, lat: f64| RowSpec {
2300 workload_kind: kind,
2301 batch_size: batch,
2302 total_prefill_tokens: prefill,
2303 total_kv_read_tokens: kv,
2304 latency_ms: lat,
2305 ..RowSpec::default()
2306 };
2307 let rows = vec![
2308 mk("prefill", 1, 2048, 0, 20.0),
2309 mk("decode", 1, 0, 2, 2.0),
2310 mk("decode", 1, 0, 64, 3.0),
2311 mk("decode", 2, 0, 4, 2.5),
2312 mk("decode", 2, 0, 64, 3.5),
2313 mk("decode", 8, 0, 16, 4.0),
2314 mk("decode", 8, 0, 64, 5.0),
2315 mk("decode", 9, 0, 18, 5.0),
2316 mk("decode", 9, 0, 64, 6.0),
2317 mk("decode", 16, 0, 32, 9.0),
2318 mk("decode", 16, 0, 64, 10.0),
2319 mk("decode", 17, 0, 34, 10.0),
2320 mk("decode", 17, 0, 64, 11.0),
2321 ];
2322 let tmp = tempfile::tempdir().unwrap();
2323 let engine = build_fpm_engine_with_rows(tmp.path(), &rows).unwrap();
2324 let mixed = ForwardPassMetrics {
2325 scheduled_requests: ScheduledRequestMetrics {
2326 num_prefill_requests: 1,
2327 sum_prefill_tokens: 2048,
2328 num_decode_requests: 15,
2329 sum_decode_kv_tokens: 64,
2330 ..Default::default()
2331 },
2332 ..Default::default()
2333 };
2334
2335 let weight = (15.0 - 9.0) / (16.0 - 9.0);
2336 let decode = 6.0 + (10.0 - 6.0) * weight;
2337 let baseline = 5.0 + (9.0 - 5.0) * weight;
2338 let expected = 20.0 + decode - baseline;
2339 let got = engine.forward_pass_time_ms(&[mixed]).unwrap();
2340 assert!((got - expected).abs() < 1e-9, "got {got}, want {expected}");
2341 }
2342
2343 #[test]
2348 fn fpm_mixed_baseline_follows_the_query_off_a_ragged_bracket_row() {
2349 use crate::fpm::{ForwardPassMetrics, ScheduledRequestMetrics};
2350 use crate::perf_database::fpm_forward::tests::RowSpec;
2351 let mk = |kind: &'static str, batch: u32, prefill: u32, kv: u32, lat: f64| RowSpec {
2352 workload_kind: kind,
2353 batch_size: batch,
2354 total_prefill_tokens: prefill,
2355 total_kv_read_tokens: kv,
2356 latency_ms: lat,
2357 ..RowSpec::default()
2358 };
2359 let rows = vec![
2362 mk("prefill", 1, 16, 0, 20.0),
2363 mk("prefill", 1, 32, 0, 40.0),
2364 mk("decode", 1, 0, 2, 2.0),
2365 mk("decode", 1, 0, 96, 3.0),
2366 mk("decode", 2, 0, 4, 2.5),
2367 mk("decode", 2, 0, 96, 3.5),
2368 mk("decode", 8, 0, 16, 4.0),
2369 mk("decode", 8, 0, 96, 5.0),
2370 mk("decode", 9, 0, 18, 5.0),
2371 mk("decode", 9, 0, 64, 6.0),
2372 mk("decode", 16, 0, 32, 9.0),
2373 mk("decode", 16, 0, 96, 10.0),
2374 mk("decode", 17, 0, 34, 10.0),
2375 mk("decode", 17, 0, 96, 11.0),
2376 ];
2377 let tmp = tempfile::tempdir().unwrap();
2378 let engine = build_fpm_engine_with_rows(tmp.path(), &rows).unwrap();
2379
2380 let ms = engine.mixed_step_latency(5, 15, 5, 0, 0, 1.0, 1.0).unwrap();
2384 let prefill = 20.0 + (40.0 - 20.0) * (20.0 - 16.0) / (32.0 - 16.0);
2385 let decode = 9.0 + (10.0 - 9.0) * (90.0 - 32.0) / (96.0 - 32.0);
2386 let expected = prefill + (decode - 9.0);
2387 assert!((ms - expected).abs() < 1e-9, "got {ms}, want {expected}");
2388
2389 let mixed = ForwardPassMetrics {
2393 scheduled_requests: ScheduledRequestMetrics {
2394 num_prefill_requests: 1,
2395 sum_prefill_tokens: 20,
2396 num_decode_requests: 15,
2397 sum_decode_kv_tokens: 80,
2398 ..Default::default()
2399 },
2400 ..Default::default()
2401 };
2402 let decode = 9.0 + (10.0 - 9.0) * (80.0 - 32.0) / (96.0 - 32.0);
2403 let expected = prefill + (decode - 9.0);
2404 let ms = engine.forward_pass_time_ms(&[mixed]).unwrap();
2405 assert!((ms - expected).abs() < 1e-9, "got {ms}, want {expected}");
2406 }
2407
2408 #[test]
2412 fn fpm_rank_uses_iteration_totals_not_averages() {
2413 use crate::fpm::{ForwardPassMetrics, ScheduledRequestMetrics};
2414 let tmp = tempfile::tempdir().unwrap();
2415 let engine = build_fpm_engine(tmp.path(), None).unwrap();
2416
2417 let decode_only = ForwardPassMetrics {
2421 scheduled_requests: ScheduledRequestMetrics {
2422 num_decode_requests: 8,
2423 sum_decode_kv_tokens: 32_773,
2424 ..Default::default()
2425 },
2426 ..Default::default()
2427 };
2428 let w = (32_773.0 - 4096.0) / (65_536.0 - 4096.0);
2429 let expected = 7.0 + (9.0 - 7.0) * w;
2430 let got = engine.forward_pass_time_ms(&[decode_only]).unwrap();
2431 assert!((got - expected).abs() < 1e-9, "got {got}, want {expected}");
2432 }
2433
2434 #[test]
2439 fn nested_fpm_op_is_rejected_at_build() {
2440 use crate::perf_database::fpm_forward::tests::default_identity;
2441 let db = PerfDatabase::load(&systems_root(), "b200_sxm", "vllm", "0.24.0").unwrap();
2442 let hidden = Op::Overlap(crate::operators::OverlapOp::new(
2443 "hidden",
2444 vec![Op::FpmForward(FpmForwardOp {
2445 name: "fpm_forward_prefill".into(),
2446 phase: FpmPhase::Prefill,
2447 model_path: "org/model-a".into(),
2448 match_identity: default_identity(4),
2449 weight_bytes: 0.0,
2450 sol_ops: vec![],
2451 })],
2452 vec![],
2453 ));
2454 let spec = EngineSpec::new(fixture_engine_config(None), vec![hidden], generation_ops());
2455 let err = Engine::build(spec, Arc::new(db)).unwrap_err();
2456 assert!(
2457 err.to_string()
2458 .contains("exactly one FpmForward op per phase"),
2459 "{err}"
2460 );
2461 }
2462
2463 #[test]
2471 fn nextn_scales_decode_batch() {
2472 let engine_nextn1 = build_engine(Some(1));
2473 assert_eq!(engine_nextn1.nextn, 1);
2474
2475 let rt = runtime(1, 1024, 2);
2478 let generation = engine_nextn1
2479 .run_static(&rt, StaticMode::Generation, 32)
2480 .unwrap();
2481 let doubled = run_generation_ops_step(
2482 &engine_nextn1.generation_ops,
2483 engine_nextn1.database(),
2484 2,
2485 1024 + 1,
2486 1.0,
2487 false,
2488 )
2489 .unwrap();
2490 assert!(
2491 (generation.generation_ms - doubled).abs() < 1e-9,
2492 "nextn=1 gen ({}) must equal the gen-step at 2*batch ({})",
2493 generation.generation_ms,
2494 doubled
2495 );
2496 }
2497
2498 #[test]
2505 fn evaluate_ops_sol_json_matches_sol_view() {
2506 use crate::perf_database::gemm::quant_tc_flops;
2507 use crate::session::query_context_op;
2508
2509 let engine = build_engine(None);
2510 let ops = context_ops();
2511 let ops_json = serde_json::to_string(&ops).unwrap();
2512 let (batch, s) = (4u32, 512u32);
2513 let sol = engine
2514 .evaluate_ops_sol_json(&ops_json, true, batch, s, 0, 1.0, None)
2515 .unwrap();
2516 assert_eq!(sol.len(), ops.len());
2517
2518 let sol_db = engine.database().sol_full_view();
2519 for (op, entry) in ops.iter().zip(&sol) {
2520 let r = query_context_op(op, &sol_db, batch, s, 0, 1.0, None).unwrap();
2521 assert_eq!(entry.0, op.name());
2522 assert!(
2523 (entry.1 - r.latency_ms).abs() < 1e-12,
2524 "{}: sol_time {} != Sol-view latency {}",
2525 entry.0,
2526 entry.1,
2527 r.latency_ms
2528 );
2529 }
2530
2531 for entry in sol.iter().take(2) {
2535 assert!(
2536 (entry.1 - entry.2.max(entry.3)).abs() < 1e-12,
2537 "{}: leaf max identity broken: {:?}",
2538 entry.0,
2539 entry
2540 );
2541 }
2542
2543 let spec = &engine.database().system_spec;
2546 let quant = GemmQuantMode::Fp8Block;
2547 let tc_flops = quant_tc_flops(spec, quant.mapping()).unwrap();
2548 let (m, n, k) = ((batch * s) as f64, 4096.0, 4096.0);
2549 let math = 2.0 * m * n * k / tc_flops * 1000.0;
2550 let mem = quant.mapping().memory * (m * n + m * k + n * k) / spec.gpu.mem_bw * 1000.0;
2551 let gemm = &sol[1];
2552 assert!(
2553 (gemm.2 - math).abs() < 1e-12,
2554 "sol_math {} != {math}",
2555 gemm.2
2556 );
2557 assert!((gemm.3 - mem).abs() < 1e-12, "sol_mem {} != {mem}", gemm.3);
2558 }
2559
2560 #[test]
2565 fn evaluate_ops_sol_json_blends_dsa_full_skip() {
2566 use crate::common::enums::{FmhaQuantMode, KvCacheQuantMode};
2567 use crate::operators::DsaModuleOp;
2568 use crate::perf_database::dsa::{dsa_context_sol, dsa_context_sol_flops, dsa_dims};
2569
2570 let engine = build_engine(None);
2571 let spec = &engine.database().system_spec;
2572 let mut op = DsaModuleOp::new(
2573 "dsa_context",
2574 128,
2575 KvCacheQuantMode::Bfloat16,
2576 FmhaQuantMode::Bfloat16,
2577 GemmQuantMode::Bfloat16,
2578 "DeepseekV32ForCausalLM",
2579 2048,
2580 );
2581 let w = 0.5;
2582 op.full_frac = w;
2583 let (b, s) = (1u32, 4096u32);
2584 let ops_json = serde_json::to_string(&vec![Op::DsaContext(op.clone())]).unwrap();
2585 let sol = engine
2586 .evaluate_ops_sol_json(&ops_json, true, b, s, 0, 1.0, None)
2587 .unwrap();
2588 assert_eq!(sol.len(), 1);
2589
2590 let dims = dsa_dims(&op.architecture);
2591 let flops = dsa_context_sol_flops(spec, op.gemm_quant_mode, op.fmha_quant_mode).unwrap();
2592 let leaf = |skip: bool| {
2593 dsa_context_sol(
2594 spec,
2595 dims,
2596 op.index_topk as i64,
2597 op.kv_cache_dtype,
2598 op.fmha_quant_mode,
2599 op.gemm_quant_mode,
2600 b as i64,
2601 s as i64,
2602 0,
2603 op.num_heads as i64,
2604 skip,
2605 flops,
2606 )
2607 };
2608 let (full, skip) = (leaf(false), leaf(true));
2609 let expected_math = w * full.math_ms + (1.0 - w) * skip.math_ms;
2610 let expected_mem = w * full.mem_ms + (1.0 - w) * skip.mem_ms;
2611 let expected_time = w * full.time_ms() + (1.0 - w) * skip.time_ms();
2612 let (_, sol_time, sol_math, sol_mem) = &sol[0];
2613 assert!(
2614 (sol_time - expected_time).abs() < 1e-12,
2615 "{sol_time} vs {expected_time}"
2616 );
2617 assert!(
2618 (sol_math - expected_math).abs() < 1e-12,
2619 "{sol_math} vs {expected_math}"
2620 );
2621 assert!(
2622 (sol_mem - expected_mem).abs() < 1e-12,
2623 "{sol_mem} vs {expected_mem}"
2624 );
2625 assert!(skip.time_ms() < full.time_ms());
2628 }
2629
2630 #[test]
2636 fn evaluate_ops_sol_json_rejects_cp_dsa_explicitly() {
2637 use crate::operators::DsaModuleOp;
2638
2639 let engine = build_engine(None);
2640 let mut op = DsaModuleOp::new(
2641 "dsa_context",
2642 64,
2643 KvCacheQuantMode::Bfloat16,
2644 FmhaQuantMode::Bfloat16,
2645 GemmQuantMode::Bfloat16,
2646 "GlmMoeDsaForCausalLM",
2647 2048,
2648 );
2649 op.cp_size = 2;
2650 op.full_frac = 0.5;
2651 let ops_json = serde_json::to_string(&vec![Op::DsaContext(op)]).unwrap();
2652 let err = engine
2653 .evaluate_ops_sol_json(&ops_json, true, 1, 4096, 0, 1.0, None)
2654 .unwrap_err();
2655
2656 match err {
2657 AicError::InvalidEngineConfig(message) => {
2658 assert!(
2659 message.contains("DSA context SOL_FULL decomposition is not supported")
2660 && message.contains("cp_size=2")
2661 && message.contains("sparse MQA/top-k deltas are latency-only"),
2662 "unexpected message: {message}"
2663 );
2664 }
2665 other => panic!("expected explicit CP DSA configuration error, got {other}"),
2666 }
2667 }
2668
2669 #[test]
2672 fn evaluate_ops_sol_json_rejects_unexported_families() {
2673 let engine = build_engine(None);
2674 let ops = vec![Op::Mamba2(crate::operators::Mamba2Op {
2675 name: "mamba2".into(),
2676 scale_factor: 1.0,
2677 kernel_source: "causal_conv1d_fn".into(),
2678 phase: "context".into(),
2679 d_model: 4096,
2680 d_state: 128,
2681 d_conv: 4,
2682 nheads: 128,
2683 head_dim: 64,
2684 n_groups: 8,
2685 chunk_size: 256,
2686 })];
2687 let ops_json = serde_json::to_string(&ops).unwrap();
2688 let err = engine
2689 .evaluate_ops_sol_json(&ops_json, true, 1, 128, 0, 1.0, None)
2690 .unwrap_err();
2691 assert!(matches!(&err, AicError::SolNotImplemented(_)));
2692 assert!(
2693 err.to_string().contains("no SOL decomposition"),
2694 "unexpected error: {err}"
2695 );
2696 }
2697}