1use serde::{Deserialize, Serialize};
26
27use crate::ENGINE_SPEC_SCHEMA_VERSION;
28use crate::common::error::AicError;
29use crate::perfmodel::EngineConfig;
30
31pub use crate::operators::op::Op as OpSpec;
34
35#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
51pub struct EngineSpec {
52 pub schema_version: u32,
53 pub engine: EngineConfig,
56 pub context_ops: Vec<OpSpec>,
59 pub generation_ops: Vec<OpSpec>,
61}
62
63#[derive(Serialize, Deserialize)]
68struct BincodeWire {
69 schema_version: u32,
70 engine_json: String,
71 context_ops: Vec<OpSpec>,
72 generation_ops: Vec<OpSpec>,
73}
74
75impl EngineSpec {
76 pub fn new(
78 engine: EngineConfig,
79 context_ops: Vec<OpSpec>,
80 generation_ops: Vec<OpSpec>,
81 ) -> Self {
82 Self {
83 schema_version: ENGINE_SPEC_SCHEMA_VERSION,
84 engine,
85 context_ops,
86 generation_ops,
87 }
88 }
89
90 pub fn to_bincode(&self) -> Result<Vec<u8>, AicError> {
94 let engine_json = serde_json::to_string(&self.engine)
95 .map_err(|e| AicError::EngineSpec(format!("engine JSON encode: {e}")))?;
96 let wire = BincodeWire {
97 schema_version: self.schema_version,
98 engine_json,
99 context_ops: self.context_ops.clone(),
100 generation_ops: self.generation_ops.clone(),
101 };
102 bincode::serialize(&wire).map_err(|e| AicError::EngineSpec(format!("bincode encode: {e}")))
103 }
104
105 pub fn from_bincode(bytes: &[u8]) -> Result<Self, AicError> {
115 let mut cursor = std::io::Cursor::new(bytes);
119 let schema_version: u32 = bincode::deserialize_from(&mut cursor).map_err(|e| {
120 AicError::EngineSpec(format!(
121 "bincode decode of the leading schema_version prefix failed \
122 (payload is {} bytes; too short or not an EngineSpec wire buffer): {e}",
123 bytes.len()
124 ))
125 })?;
126 if schema_version != ENGINE_SPEC_SCHEMA_VERSION {
127 return Err(AicError::UnsupportedSchemaVersion {
128 kind: "EngineSpec",
129 got: schema_version,
130 expected: ENGINE_SPEC_SCHEMA_VERSION,
131 });
132 }
133 let wire: BincodeWire = bincode::deserialize(bytes).map_err(|e| {
134 AicError::EngineSpec(format!(
135 "bincode decode of the op payloads failed at matching \
136 schema_version {schema_version} — this indicates op-layout drift \
137 within the same version (an OpSpec changed without a \
138 ENGINE_SPEC_SCHEMA_VERSION bump) or a corrupt payload, not a \
139 version skew: {e}"
140 ))
141 })?;
142 let engine: EngineConfig = serde_json::from_str(&wire.engine_json).map_err(|e| {
143 AicError::EngineSpec(format!(
144 "engine JSON decode failed at schema_version {schema_version}: {e}"
145 ))
146 })?;
147 Ok(Self {
148 schema_version: wire.schema_version,
149 engine,
150 context_ops: wire.context_ops,
151 generation_ops: wire.generation_ops,
152 })
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use std::collections::BTreeMap;
160
161 use crate::common::enums::{
162 BackendKind, CommQuantMode, FmhaQuantMode, GemmQuantMode, KvCacheQuantMode, MoeQuantMode,
163 };
164 use crate::operators::moe_dispatch::DispatchFlavor;
165 use crate::operators::op::{FallbackOp, OverlapOp};
166 use crate::operators::{
167 ContextAttentionOp, ContextMlaOp, CustomAllReduceOp, DsaModuleOp, Dsv4MegaMoeOp,
168 Dsv4ModuleOp, ElementwiseOp, EmbeddingOp, EncoderAttentionOp, GdnOp, GemmOp,
169 GenerationAttentionOp, GenerationMlaOp, KdaOp, Mamba2Op, MhcModuleOp, MlaBmmOp,
170 MlaModuleOp, MoEDispatchOp, MoeAllToAllOp, MoeExpertComputeOp, MoeOp, NcclOp, P2POp,
171 VisionEncoderOp, WideEpContextMlaOp, WideEpGenerationMlaOp,
172 };
173 use crate::perf_database::dsv4::AttnKind;
174 use crate::{
175 DataType, ENGINE_CONFIG_SCHEMA_VERSION, ParallelMapping, QuantizationConfig,
176 SpeculativeConfig,
177 };
178
179 fn gemm() -> GemmOp {
182 GemmOp {
183 name: "qkv_gemm".into(),
184 scale_factor: 2.0,
185 n: 4096,
186 k: 4096,
187 quant_mode: GemmQuantMode::Fp8,
188 scale_num_tokens: 0,
189 low_precision_input: true,
190 seq_split: 1,
191 below_grid_sol: false,
192 }
193 }
194
195 fn embedding() -> EmbeddingOp {
196 EmbeddingOp {
197 name: "embedding".into(),
198 scale_factor: 1.0,
199 vocab_size: 128_256,
200 hidden_size: 4096,
201 quant_mode: GemmQuantMode::Bfloat16,
202 seq_split: 1,
203 }
204 }
205
206 fn elementwise() -> ElementwiseOp {
207 ElementwiseOp {
208 name: "rmsnorm".into(),
209 scale_factor: 1.5,
210 bytes_per_token: 8192.0,
211 scale_num_tokens: 1,
212 seq_split: 1,
213 }
214 }
215
216 fn context_attention() -> ContextAttentionOp {
217 ContextAttentionOp {
218 name: "context_attention".into(),
219 scale_factor: 1.0,
220 n: 32,
221 n_kv: 8,
222 head_size: 128,
223 window_size: 0,
224 kv_cache_dtype: KvCacheQuantMode::Fp8,
225 fmha_quant_mode: FmhaQuantMode::Bfloat16,
226 use_qk_norm: true,
227 cp_size: 1,
228 lane_order: vec!["trtllm_mha".into(), "flashinfer".into(), "default".into()],
231 }
232 }
233
234 fn generation_attention() -> GenerationAttentionOp {
235 GenerationAttentionOp {
236 name: "generation_attention".into(),
237 scale_factor: 1.0,
238 n: 32,
239 n_kv: 8,
240 head_size: 128,
241 window_size: 4096,
242 kv_cache_dtype: KvCacheQuantMode::Int8,
243 lane_order: vec!["triton".into(), "trtllm_mha".into(), "default".into()],
244 }
245 }
246
247 fn encoder_attention() -> EncoderAttentionOp {
248 EncoderAttentionOp {
249 name: "encoder_attention".into(),
250 scale_factor: 1.0,
251 n: 16,
252 head_size: 80,
253 fmha_quant_mode: FmhaQuantMode::Fp8,
254 partial_rotary_factor: 0.0,
255 }
256 }
257
258 fn context_mla() -> ContextMlaOp {
259 ContextMlaOp {
260 name: "context_mla".into(),
261 scale_factor: 1.0,
262 num_heads: 128,
263 kv_cache_dtype: KvCacheQuantMode::Bfloat16,
264 fmha_quant_mode: FmhaQuantMode::Bfloat16,
265 cp_size: 1,
266 }
267 }
268
269 fn generation_mla() -> GenerationMlaOp {
270 GenerationMlaOp {
271 name: "generation_mla".into(),
272 scale_factor: 1.0,
273 num_heads: 128,
274 kv_cache_dtype: KvCacheQuantMode::Fp8,
275 }
276 }
277
278 fn mla_module() -> MlaModuleOp {
279 MlaModuleOp {
280 name: "context_mla_module".into(),
281 scale_factor: 1.0,
282 num_heads: 128,
283 kv_cache_dtype: KvCacheQuantMode::Fp8,
284 fmha_quant_mode: FmhaQuantMode::Fp8,
285 gemm_quant_mode: GemmQuantMode::Fp8Block,
286 native_num_heads: Some(128),
287 }
288 }
289
290 fn mla_bmm() -> MlaBmmOp {
291 MlaBmmOp {
292 name: "mla_bmm_pre".into(),
293 scale_factor: 1.0,
294 num_heads: 128,
295 quant_mode: GemmQuantMode::Bfloat16,
296 is_pre: true,
297 }
298 }
299
300 fn moe() -> MoeOp {
301 MoeOp {
302 name: "moe".into(),
303 scale_factor: 1.0,
304 hidden_size: 7168,
305 inter_size: 2048,
306 topk: 8,
307 num_experts: 256,
308 moe_tp_size: 1,
309 moe_ep_size: 8,
310 attention_dp_size: 1,
311 quant_mode: MoeQuantMode::Fp8Block,
312 workload_distribution: "power_law_1.2".into(),
313 is_gated: true,
314 moe_backend: None,
315 enable_eplb: false,
316 is_context: false,
317 }
318 }
319
320 fn moe_dispatch() -> MoEDispatchOp {
321 MoEDispatchOp {
322 name: "moe_dispatch".into(),
323 scale_factor: 1.0,
324 hidden_size: 7168,
325 topk: 8,
326 num_experts: 256,
327 moe_tp_size: 1,
328 moe_ep_size: 8,
329 attention_dp_size: 8,
330 pre_dispatch: true,
331 backend: BackendKind::Trtllm,
332 flavor: DispatchFlavor::TrtllmAlltoall,
333 comm_quant: CommQuantMode::Half,
334 moe_quant: MoeQuantMode::Fp8Block,
335 attn_cp_size: 1,
336 is_context: false,
337 sms: 12,
338 scale_num_tokens: 1,
339 attn_ar_modeled: false,
340 }
341 }
342
343 fn custom_all_reduce() -> CustomAllReduceOp {
344 CustomAllReduceOp {
345 name: "custom_all_reduce".into(),
346 scale_factor: 1.0,
347 hidden_size: 4096,
348 tp_size: 8,
349 quant: CommQuantMode::Half,
350 seq_split: 1,
351 }
352 }
353
354 fn nccl() -> NcclOp {
355 NcclOp {
356 name: "nccl_all_reduce".into(),
357 scale_factor: 1.0,
358 hidden_size: 4096.0,
359 num_gpus: 8,
360 dtype: CommQuantMode::Half,
361 operation: "all_reduce".into(),
362 seq_split: 1,
363 }
364 }
365
366 fn p2p() -> P2POp {
367 P2POp {
368 name: "p2p".into(),
369 scale_factor: 1.0,
370 pp_size: 4,
371 hidden_size: 4096,
372 seq_split: 1,
373 }
374 }
375
376 fn vision() -> VisionEncoderOp {
377 VisionEncoderOp {
378 name: "vision_encoder".into(),
379 scale_factor: 1.0,
380 num_layers: 24,
381 num_heads: 16,
382 head_size: 80,
383 hidden_size: 1280,
384 intermediate_size: 5120,
385 fmha_quant: FmhaQuantMode::Bfloat16,
386 gemm_quant: GemmQuantMode::Bfloat16,
387 }
388 }
389
390 fn dsa_module() -> DsaModuleOp {
391 DsaModuleOp {
392 name: "dsa_module".into(),
393 scale_factor: 1.0,
394 num_heads: 128,
395 kv_cache_dtype: KvCacheQuantMode::Fp8,
396 fmha_quant_mode: FmhaQuantMode::Fp8,
397 gemm_quant_mode: GemmQuantMode::Fp8Block,
398 architecture: "DeepseekV32ForCausalLM".into(),
399 index_topk: 2048,
400 cp_size: 1,
401 full_frac: 1.0,
402 attn_projection_quant_modes: None,
403 }
404 }
405
406 fn msa_module() -> crate::operators::MsaModuleOp {
407 crate::operators::MsaModuleOp {
408 name: "context_attention".into(),
409 scale_factor: 62.0,
410 num_heads: 8,
411 num_kv_heads: 1,
412 hidden_size: 7168,
413 head_dim: 128,
414 v_head_dim: 128,
415 index_n_heads: 64,
416 index_head_dim: 128,
417 index_topk: 2048,
418 block_size: 64,
419 kv_cache_dtype: KvCacheQuantMode::Bfloat16,
420 fmha_quant_mode: FmhaQuantMode::Bfloat16,
421 gemm_quant_mode: GemmQuantMode::Fp8Block,
422 dsa_architecture: "GlmMoeDsaForCausalLM".into(),
423 dsa_scale_k: 1.0,
424 }
425 }
426
427 fn dsv4_module() -> Dsv4ModuleOp {
428 Dsv4ModuleOp {
429 name: "dsv4_module".into(),
430 scale_factor: 1.0,
431 attn_kind: AttnKind::Hca,
432 num_heads: 128,
433 native_heads: 128,
434 tp_size: 1,
435 kv_cache_dtype: KvCacheQuantMode::Fp8,
436 fmha_quant_mode: FmhaQuantMode::Fp8,
437 gemm_quant_mode: GemmQuantMode::Fp8Block,
438 architecture: "DeepseekV4ForCausalLM".into(),
439 cp_size: 1,
440 window_size: None,
441 hidden_size: 7168,
442 q_lora_rank: 1536,
443 o_lora_rank: 1024,
444 head_dim: 512,
445 rope_head_dim: 64,
446 index_n_heads: 64,
447 index_head_dim: 128,
448 index_topk: 1024,
449 o_groups: Some(16),
450 }
451 }
452
453 fn dsv4_megamoe() -> Dsv4MegaMoeOp {
454 Dsv4MegaMoeOp {
455 name: "context_megamoe".into(),
456 scale_factor: 61.0,
457 hidden_size: 7168,
458 inter_size: 3072,
459 topk: 6,
460 num_experts: 384,
461 moe_tp_size: 1,
462 moe_ep_size: 8,
463 quant_mode: MoeQuantMode::W4a8Mxfp4Mxfp8,
464 workload_distribution: "balanced".into(),
465 is_context: true,
466 source_policy: "random".into(),
467 pre_dispatch: "sglang_jit".into(),
468 num_fused_shared_experts: 0,
469 kernel_source: "deepgemm_megamoe".into(),
470 kernel_dtype: "fp8_fp4".into(),
471 }
472 }
473
474 fn mhc() -> MhcModuleOp {
475 MhcModuleOp {
476 name: "mhc_module".into(),
477 scale_factor: 1.0,
478 op: "pre".into(),
479 hc_mult: 4,
480 hidden_size: 7168,
481 architecture: "DeepseekV4ForCausalLM".into(),
482 sinkhorn_iters: 20,
483 quant_mode: GemmQuantMode::Bfloat16,
484 seq_split: 1,
485 }
486 }
487
488 fn mamba2() -> Mamba2Op {
489 Mamba2Op {
490 name: "mamba2".into(),
491 scale_factor: 1.0,
492 kernel_source: "mamba_chunk_scan".into(),
493 phase: "context".into(),
494 d_model: 4096,
495 d_state: 128,
496 d_conv: 4,
497 nheads: 128,
498 head_dim: 64,
499 n_groups: 8,
500 chunk_size: 256,
501 }
502 }
503
504 fn gdn() -> GdnOp {
505 GdnOp {
506 name: "gdn".into(),
507 scale_factor: 1.0,
508 kernel_source: "gdn_kernel".into(),
509 phase: "generation".into(),
510 d_model: 4096,
511 d_conv: 4,
512 num_k_heads: 16,
513 head_k_dim: 128,
514 num_v_heads: 32,
515 head_v_dim: 128,
516 mamba_ssm_dtype: "bfloat16".into(),
519 }
520 }
521
522 fn kda() -> KdaOp {
523 KdaOp {
524 name: "kda".into(),
525 scale_factor: 1.0,
526 kernel_source: "fused_sigmoid_gating_delta_rule_update".into(),
527 phase: "verify".into(),
528 d_model: 7168,
529 d_conv: 4,
530 num_k_heads: 16,
531 head_k_dim: 128,
532 num_v_heads: 16,
533 head_v_dim: 128,
534 draft_tokens: 4,
535 }
536 }
537
538 fn wideep_context_mla() -> WideEpContextMlaOp {
539 WideEpContextMlaOp {
540 name: "wideep_context_mla".into(),
541 scale_factor: 1.0,
542 num_heads: 128,
543 kv_cache_dtype: KvCacheQuantMode::Fp8,
544 fmha_quant_mode: FmhaQuantMode::Fp8,
545 attn_backend: "flashinfer".into(),
546 cp_size: 1,
547 }
548 }
549
550 fn wideep_generation_mla() -> WideEpGenerationMlaOp {
551 WideEpGenerationMlaOp {
552 name: "wideep_generation_mla".into(),
553 scale_factor: 1.0,
554 num_heads: 128,
555 kv_cache_dtype: KvCacheQuantMode::Fp8,
556 fmha_quant_mode: FmhaQuantMode::Fp8,
557 attn_backend: "flashinfer".into(),
558 }
559 }
560
561 fn moe_all_to_all() -> MoeAllToAllOp {
565 MoeAllToAllOp {
566 name: "moe_dispatch".into(),
567 scale_factor: 61.0,
568 phase: "dispatch".into(),
569 comm_backend: "deepep_ht".into(),
570 comm_dtype: "fp8_block".into(),
571 hidden_size: 7168,
572 topk: 8,
573 num_experts: 256,
574 moe_ep_size: 16,
575 node_num: 2,
576 sms: 24,
577 attention_tp_size: 2,
578 }
579 }
580
581 fn moe_expert_compute() -> MoeExpertComputeOp {
585 MoeExpertComputeOp {
586 name: "moe".into(),
587 scale_factor: 61.0,
588 hidden_size: 7168,
589 inter_size: 2048,
590 topk: 8,
591 num_experts: 256,
592 moe_ep_size: 16,
593 quant_mode: MoeQuantMode::Fp8Block,
594 workload_distribution: "power_law_1.2".into(),
595 attention_dp_size: 8,
596 inference_phase: "context".into(),
597 num_slots: Some(288),
598 kernel_source: Some("deepep_moe".into()),
599 is_gated: true,
600 enable_eplb: true,
601 }
602 }
603
604 fn overlap() -> OverlapOp {
605 OverlapOp {
607 name: "overlap_attn_moe".into(),
608 group_a: vec![OpSpec::ContextMla(context_mla()), OpSpec::Gemm(gemm())],
609 group_b: vec![OpSpec::Moe(moe()), OpSpec::MoeDispatch(moe_dispatch())],
610 }
611 }
612
613 fn fpm_forward() -> crate::operators::FpmForwardOp {
614 crate::operators::FpmForwardOp {
617 name: "fpm_forward_prefill".into(),
618 phase: crate::operators::FpmPhase::Prefill,
619 model_path: "org/model-a".into(),
620 match_identity: vec![
621 "nvfp4".into(),
622 "nvfp4".into(),
623 "bfloat16".into(),
624 "half".into(),
625 "fp8".into(),
626 "4".into(),
627 "1".into(),
628 "1".into(),
629 "4".into(),
630 "1".into(),
631 "1".into(),
632 ],
633 weight_bytes: 1.5e10,
634 sol_ops: vec![
635 OpSpec::Gemm(gemm()),
636 OpSpec::ContextAttention(context_attention()),
637 ],
638 }
639 }
640
641 fn fallback() -> FallbackOp {
642 FallbackOp {
645 name: "mla_fallback".into(),
646 primary: Box::new(OpSpec::MlaModuleContext(mla_module())),
647 fallback: vec![
648 OpSpec::Gemm(gemm()),
649 OpSpec::ContextMla(context_mla()),
650 OpSpec::Overlap(overlap()),
651 ],
652 }
653 }
654
655 fn all_op_variants() -> Vec<OpSpec> {
659 let ops = vec![
660 OpSpec::Gemm(gemm()),
661 OpSpec::Embedding(embedding()),
662 OpSpec::Elementwise(elementwise()),
663 OpSpec::ContextAttention(context_attention()),
664 OpSpec::GenerationAttention(generation_attention()),
665 OpSpec::EncoderAttention(encoder_attention()),
666 OpSpec::ContextMla(context_mla()),
667 OpSpec::GenerationMla(generation_mla()),
668 OpSpec::MlaModuleContext(mla_module()),
669 OpSpec::MlaModuleGeneration(mla_module()),
670 OpSpec::MlaBmm(mla_bmm()),
671 OpSpec::Moe(moe()),
672 OpSpec::MoeDispatch(moe_dispatch()),
673 OpSpec::CustomAllReduce(custom_all_reduce()),
674 OpSpec::Nccl(nccl()),
675 OpSpec::P2P(p2p()),
676 OpSpec::Vision(vision()),
677 OpSpec::DsaContext(dsa_module()),
678 OpSpec::DsaGeneration(dsa_module()),
679 OpSpec::MsaContext(msa_module()),
680 OpSpec::MsaGeneration(msa_module()),
681 OpSpec::Dsv4Context(dsv4_module()),
682 OpSpec::Dsv4Generation(dsv4_module()),
683 OpSpec::Mhc(mhc()),
684 OpSpec::Mamba2(mamba2()),
685 OpSpec::Gdn(gdn()),
686 OpSpec::WideEpContextMla(wideep_context_mla()),
687 OpSpec::WideEpGenerationMla(wideep_generation_mla()),
688 OpSpec::Overlap(overlap()),
689 OpSpec::Fallback(fallback()),
690 OpSpec::Dsv4MegaMoe(dsv4_megamoe()),
693 OpSpec::Kda(kda()),
696 OpSpec::FpmForward(fpm_forward()),
697 OpSpec::MoeAllToAll(moe_all_to_all()),
698 OpSpec::MoeExpertCompute(moe_expert_compute()),
699 ];
700
701 for op in &ops {
704 match op {
705 OpSpec::Gemm(_)
706 | OpSpec::Embedding(_)
707 | OpSpec::Elementwise(_)
708 | OpSpec::ContextAttention(_)
709 | OpSpec::GenerationAttention(_)
710 | OpSpec::EncoderAttention(_)
711 | OpSpec::ContextMla(_)
712 | OpSpec::GenerationMla(_)
713 | OpSpec::MlaModuleContext(_)
714 | OpSpec::MlaModuleGeneration(_)
715 | OpSpec::MlaBmm(_)
716 | OpSpec::Moe(_)
717 | OpSpec::MoeDispatch(_)
718 | OpSpec::CustomAllReduce(_)
719 | OpSpec::Nccl(_)
720 | OpSpec::P2P(_)
721 | OpSpec::Vision(_)
722 | OpSpec::DsaContext(_)
723 | OpSpec::DsaGeneration(_)
724 | OpSpec::MsaContext(_)
725 | OpSpec::MsaGeneration(_)
726 | OpSpec::Dsv4Context(_)
727 | OpSpec::Dsv4Generation(_)
728 | OpSpec::Mhc(_)
729 | OpSpec::Mamba2(_)
730 | OpSpec::Gdn(_)
731 | OpSpec::WideEpContextMla(_)
732 | OpSpec::WideEpGenerationMla(_)
733 | OpSpec::FpmForward(_)
734 | OpSpec::Overlap(_)
735 | OpSpec::Fallback(_)
736 | OpSpec::Dsv4MegaMoe(_)
737 | OpSpec::Kda(_)
738 | OpSpec::MoeAllToAll(_)
739 | OpSpec::MoeExpertCompute(_) => {}
740 }
741 }
742 ops
743 }
744
745 fn sample_engine_config() -> EngineConfig {
746 EngineConfig {
747 schema_version: ENGINE_CONFIG_SCHEMA_VERSION,
748 model_name: "deepseek-ai/DeepSeek-V3".into(),
749 system_name: "h200_sxm".into(),
750 systems_path: None,
751 backend: crate::BackendKind::Trtllm,
752 backend_version: Some("1.0.0rc3".into()),
753 forward_model: None,
754 kv_block_size: Some(64),
755 parallel: ParallelMapping {
756 tp_size: 8,
757 pp_size: 1,
758 attention_dp_size: Some(8),
759 moe_tp_size: Some(1),
760 moe_ep_size: Some(8),
761 cp_size: None,
762 },
763 quantization: QuantizationConfig {
764 weight_dtype: Some(DataType::Fp8),
765 moe_dtype: Some(DataType::Fp8),
766 activation_dtype: Some(DataType::Fp8),
767 kv_cache_dtype: Some(DataType::Fp8),
768 },
769 speculative: Some(SpeculativeConfig { nextn: Some(1) }),
770 enable_shared_layer: None,
771 strict_provenance: false,
772 database_mode: Default::default(),
773 tolerate_dirless_version: false,
774 transfer_policy: None,
775 extra: BTreeMap::new(),
776 }
777 }
778
779 #[test]
788 fn op_variant_indices_are_pinned() {
789 const GEMM_INDEX: u32 = 0;
790 const MOE_ALL_TO_ALL_INDEX: u32 = 33;
793 const MOE_EXPERT_COMPUTE_INDEX: u32 = 34;
794
795 let index_of = |op: &OpSpec| -> u32 {
796 let bytes = bincode::serialize(op).expect("serialize op");
797 u32::from_le_bytes(bytes[..4].try_into().expect("4-byte variant index prefix"))
798 };
799
800 assert_eq!(
801 index_of(&OpSpec::Gemm(gemm())),
802 GEMM_INDEX,
803 "first variant moved"
804 );
805 assert_eq!(
806 index_of(&OpSpec::MoeAllToAll(moe_all_to_all())),
807 MOE_ALL_TO_ALL_INDEX,
808 "MoeAllToAll index moved"
809 );
810 assert_eq!(
811 index_of(&OpSpec::MoeExpertCompute(moe_expert_compute())),
812 MOE_EXPERT_COMPUTE_INDEX,
813 "MoeExpertCompute index moved"
814 );
815
816 assert_eq!(MOE_EXPERT_COMPUTE_INDEX, MOE_ALL_TO_ALL_INDEX + 1);
819 assert_eq!(
820 MOE_EXPERT_COMPUTE_INDEX as usize + 1,
821 all_op_variants().len(),
822 "all_op_variants() must cover exactly the pinned variant count"
823 );
824 }
825
826 #[test]
827 fn every_op_variant_round_trips_through_bincode() {
828 for op in all_op_variants() {
829 let bytes = bincode::serialize(&op).expect("serialize op");
830 let decoded: OpSpec = bincode::deserialize(&bytes).expect("deserialize op");
831 assert_eq!(op, decoded, "round-trip mismatch for {:?}", op);
832 }
833 }
834
835 #[test]
836 fn recursive_overlap_round_trips_with_nested_children() {
837 let op = OpSpec::Overlap(overlap());
838 let bytes = bincode::serialize(&op).unwrap();
839 let decoded: OpSpec = bincode::deserialize(&bytes).unwrap();
840 assert_eq!(op, decoded);
841 }
842
843 #[test]
844 fn recursive_fallback_round_trips_with_nested_children() {
845 let op = OpSpec::Fallback(fallback());
846 let bytes = bincode::serialize(&op).unwrap();
847 let decoded: OpSpec = bincode::deserialize(&bytes).unwrap();
848 assert_eq!(op, decoded);
849 }
850
851 #[test]
852 fn mla_module_none_native_round_trips_followed_by_another_op() {
853 let mut none_native = mla_module();
857 none_native.native_num_heads = None;
858 let spec = EngineSpec::new(
859 sample_engine_config(),
860 vec![OpSpec::MlaModuleContext(none_native), OpSpec::Gemm(gemm())],
861 vec![
862 OpSpec::MlaModuleGeneration(mla_module()),
863 OpSpec::Moe(moe()),
864 ],
865 );
866 let bytes = spec.to_bincode().expect("to_bincode");
867 let decoded = EngineSpec::from_bincode(&bytes).expect("from_bincode");
868 assert_eq!(spec, decoded);
869 }
870
871 #[test]
872 fn engine_spec_round_trips_through_bincode() {
873 let spec = EngineSpec::new(
874 sample_engine_config(),
875 vec![
876 OpSpec::Embedding(embedding()),
877 OpSpec::ContextAttention(context_attention()),
878 OpSpec::Overlap(overlap()),
879 OpSpec::Gemm(gemm()),
880 ],
881 vec![
882 OpSpec::GenerationAttention(generation_attention()),
883 OpSpec::Fallback(fallback()),
884 OpSpec::Moe(moe()),
885 ],
886 );
887
888 assert_eq!(spec.schema_version, ENGINE_SPEC_SCHEMA_VERSION);
889
890 let bytes = spec.to_bincode().expect("to_bincode");
891 let decoded = EngineSpec::from_bincode(&bytes).expect("from_bincode");
892 assert_eq!(spec, decoded);
893 }
894
895 #[test]
904 fn version_skew_reports_unsupported_before_payload_decode() {
905 let spec = EngineSpec::new(
906 sample_engine_config(),
907 vec![
908 OpSpec::Gemm(gemm()),
909 OpSpec::ContextAttention(context_attention()),
910 ],
911 vec![OpSpec::GenerationAttention(generation_attention())],
912 );
913 let mut bytes = spec.to_bincode().expect("to_bincode");
914
915 let foreign = ENGINE_SPEC_SCHEMA_VERSION + 1;
918 bytes[..4].copy_from_slice(&foreign.to_le_bytes());
919 bytes.truncate(bytes.len() - 8);
922
923 match EngineSpec::from_bincode(&bytes) {
924 Err(AicError::UnsupportedSchemaVersion {
925 kind,
926 got,
927 expected,
928 }) => {
929 assert_eq!(kind, "EngineSpec");
930 assert_eq!(got, foreign);
931 assert_eq!(expected, ENGINE_SPEC_SCHEMA_VERSION);
932 }
933 other => {
934 panic!("expected UnsupportedSchemaVersion before payload decode, got {other:?}")
935 }
936 }
937 }
938
939 fn handshake_spec() -> EngineSpec {
941 EngineSpec::new(
942 sample_engine_config(),
943 vec![
944 OpSpec::Gemm(gemm()),
945 OpSpec::ContextAttention(context_attention()),
946 ],
947 vec![OpSpec::GenerationAttention(generation_attention())],
948 )
949 }
950
951 #[test]
953 fn from_bincode_round_trips_and_preserves_version() {
954 let spec = handshake_spec();
955 let decoded = EngineSpec::from_bincode(&spec.to_bincode().expect("to_bincode"))
956 .expect("from_bincode");
957 assert_eq!(decoded, spec);
958 assert_eq!(decoded.schema_version, ENGINE_SPEC_SCHEMA_VERSION);
959 }
960
961 #[test]
964 fn from_bincode_rejects_empty_buffer() {
965 match EngineSpec::from_bincode(&[]) {
966 Err(AicError::EngineSpec(msg)) => {
967 assert!(
968 msg.contains("schema_version prefix"),
969 "message should name the prefix stage, got: {msg}"
970 );
971 }
972 other => panic!("expected EngineSpec prefix error, got {other:?}"),
973 }
974 }
975
976 #[test]
979 fn from_bincode_version_gate_fires_with_intact_payload() {
980 let mut bytes = handshake_spec().to_bincode().expect("to_bincode");
981 let foreign = ENGINE_SPEC_SCHEMA_VERSION + 7;
982 bytes[..4].copy_from_slice(&foreign.to_le_bytes()); match EngineSpec::from_bincode(&bytes) {
985 Err(AicError::UnsupportedSchemaVersion {
986 kind,
987 got,
988 expected,
989 }) => {
990 assert_eq!(kind, "EngineSpec");
991 assert_eq!(got, foreign);
992 assert_eq!(expected, ENGINE_SPEC_SCHEMA_VERSION);
993 }
994 other => panic!("expected UnsupportedSchemaVersion, got {other:?}"),
995 }
996 }
997
998 #[test]
1005 fn from_bincode_rejects_v11_dsa_producer_at_the_version_gate() {
1006 let spec = EngineSpec::new(
1007 sample_engine_config(),
1008 vec![OpSpec::DsaContext(dsa_module())],
1009 vec![],
1010 );
1011 let mut bytes = spec.to_bincode().expect("to_bincode");
1012 bytes[..4].copy_from_slice(&11u32.to_le_bytes()); match EngineSpec::from_bincode(&bytes) {
1015 Err(AicError::UnsupportedSchemaVersion {
1016 kind,
1017 got,
1018 expected,
1019 }) => {
1020 assert_eq!(kind, "EngineSpec");
1021 assert_eq!(got, 11);
1022 assert_eq!(expected, ENGINE_SPEC_SCHEMA_VERSION);
1023 }
1024 other => {
1025 panic!("expected UnsupportedSchemaVersion for a v11 DSA payload, got {other:?}")
1026 }
1027 }
1028 }
1029
1030 #[test]
1035 fn from_bincode_rejects_v13_gdn_producer_at_the_version_gate() {
1036 let mut legacy_gdn = gdn();
1037 legacy_gdn.mamba_ssm_dtype.clear();
1038 let spec = EngineSpec::new(
1039 sample_engine_config(),
1040 vec![],
1041 vec![OpSpec::Gdn(legacy_gdn)],
1042 );
1043 let mut bytes = spec.to_bincode().expect("to_bincode");
1044
1045 assert_eq!(&bytes[bytes.len() - 8..], &[0; 8]);
1049 bytes.truncate(bytes.len() - 8);
1050 bytes[..4].copy_from_slice(&13u32.to_le_bytes());
1051
1052 match EngineSpec::from_bincode(&bytes) {
1053 Err(AicError::UnsupportedSchemaVersion {
1054 kind,
1055 got,
1056 expected,
1057 }) => {
1058 assert_eq!(kind, "EngineSpec");
1059 assert_eq!(got, 13);
1060 assert_eq!(expected, ENGINE_SPEC_SCHEMA_VERSION);
1061 }
1062 other => {
1063 panic!("expected UnsupportedSchemaVersion for a v13 GDN payload, got {other:?}")
1064 }
1065 }
1066 }
1067
1068 #[test]
1072 fn from_bincode_matching_version_corrupt_ops_names_op_payload_stage() {
1073 let mut bytes = handshake_spec().to_bincode().expect("to_bincode");
1074 bytes.truncate(bytes.len() - 8);
1076
1077 match EngineSpec::from_bincode(&bytes) {
1078 Err(AicError::EngineSpec(msg)) => {
1079 assert!(
1080 msg.contains("op payloads"),
1081 "message should name the op-payload stage, got: {msg}"
1082 );
1083 assert!(
1084 msg.contains(&ENGINE_SPEC_SCHEMA_VERSION.to_string()),
1085 "message should cite the matching version, got: {msg}"
1086 );
1087 }
1088 other => panic!("expected EngineSpec op-payload error, got {other:?}"),
1089 }
1090 }
1091
1092 #[test]
1096 fn from_bincode_invalid_engine_json_names_json_stage() {
1097 let wire = BincodeWire {
1100 schema_version: ENGINE_SPEC_SCHEMA_VERSION,
1101 engine_json: "this is not json".to_string(),
1102 context_ops: vec![],
1103 generation_ops: vec![],
1104 };
1105 let bytes = bincode::serialize(&wire).expect("serialize wire");
1106
1107 match EngineSpec::from_bincode(&bytes) {
1108 Err(AicError::EngineSpec(msg)) => {
1109 assert!(
1110 msg.contains("engine JSON"),
1111 "message should name the engine-JSON stage, got: {msg}"
1112 );
1113 }
1114 other => panic!("expected EngineSpec engine-JSON error, got {other:?}"),
1115 }
1116 }
1117}