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