1use std::path::Path;
23use std::sync::{
24 atomic::{AtomicU64, Ordering},
25 OnceLock,
26};
27
28use candle_core::quantized::GgmlDType;
29use candle_core::{Device, Result as CandleResult};
30use ferrum_kernels::backend::cpu::CpuBackend;
31use ferrum_kernels::backend::{
32 Backend, BackendMoeFused, BackendPagedKv, BackendQuantGguf, BackendQuantMarlin, GgufQuantType,
33 LlmBackend, QuantLlmBackend,
34};
35use ferrum_kernels::{Linear, StackedExpertGgufLinear};
36use ferrum_quantization::gguf::GgufFile;
37use ferrum_quantization::{DenseLinear, QuantLinear};
38use ferrum_types::{FerrumError, Result};
39
40use crate::moe::router::RouterOutput;
41
42pub static MOE_SYNC_US: AtomicU64 = AtomicU64::new(0);
46pub static MOE_SYNC_CALLS: AtomicU64 = AtomicU64::new(0);
47pub static MOE_GEMV_GATE_UP_US: AtomicU64 = AtomicU64::new(0);
48pub static MOE_GEMV_GATE_UP_CALLS: AtomicU64 = AtomicU64::new(0);
49pub static MOE_SILU_US: AtomicU64 = AtomicU64::new(0);
50pub static MOE_SILU_CALLS: AtomicU64 = AtomicU64::new(0);
51pub static MOE_GEMV_DOWN_US: AtomicU64 = AtomicU64::new(0);
52pub static MOE_GEMV_DOWN_CALLS: AtomicU64 = AtomicU64::new(0);
53pub static MOE_SCALED_ADD_US: AtomicU64 = AtomicU64::new(0);
54pub static MOE_SCALED_ADD_CALLS: AtomicU64 = AtomicU64::new(0);
55pub static MOE_COPY_US: AtomicU64 = AtomicU64::new(0);
56pub static MOE_COPY_CALLS: AtomicU64 = AtomicU64::new(0);
57pub static MOE_HOST_TOPK_US: AtomicU64 = AtomicU64::new(0);
58pub static MOE_HOST_TOPK_CALLS: AtomicU64 = AtomicU64::new(0);
59
60pub static MOE_BUCKET_SYNC_US: AtomicU64 = AtomicU64::new(0);
63pub static MOE_BUCKET_D2H_US: AtomicU64 = AtomicU64::new(0);
64pub static MOE_BUCKET_ROUTE_US: AtomicU64 = AtomicU64::new(0);
65pub static MOE_BUCKET_PLAN_US: AtomicU64 = AtomicU64::new(0);
66pub static MOE_BUCKET_GATHER_US: AtomicU64 = AtomicU64::new(0);
67pub static MOE_BUCKET_GEMM1_US: AtomicU64 = AtomicU64::new(0);
68pub static MOE_BUCKET_SILU_US: AtomicU64 = AtomicU64::new(0);
69pub static MOE_BUCKET_GEMM3_US: AtomicU64 = AtomicU64::new(0);
70pub static MOE_BUCKET_COMBINE_US: AtomicU64 = AtomicU64::new(0);
71pub static MOE_BUCKET_LAYER_CALLS: AtomicU64 = AtomicU64::new(0);
72
73#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
74pub struct MoeBucketProfileSnapshot {
75 pub layers: u64,
76 pub sync_us: u64,
77 pub d2h_us: u64,
78 pub route_us: u64,
79 pub plan_us: u64,
80 pub gather_us: u64,
81 pub gemm1_us: u64,
82 pub silu_us: u64,
83 pub gemm3_us: u64,
84 pub combine_us: u64,
85}
86
87impl MoeBucketProfileSnapshot {
88 pub fn total_us(self) -> u64 {
89 self.sync_us
90 + self.d2h_us
91 + self.route_us
92 + self.plan_us
93 + self.gather_us
94 + self.gemm1_us
95 + self.silu_us
96 + self.gemm3_us
97 + self.combine_us
98 }
99
100 pub fn has_layers(self) -> bool {
101 self.layers > 0
102 }
103}
104
105pub fn drain_moe_bucket_profile() -> MoeBucketProfileSnapshot {
106 MoeBucketProfileSnapshot {
107 layers: MOE_BUCKET_LAYER_CALLS.swap(0, Ordering::Relaxed),
108 sync_us: MOE_BUCKET_SYNC_US.swap(0, Ordering::Relaxed),
109 d2h_us: MOE_BUCKET_D2H_US.swap(0, Ordering::Relaxed),
110 route_us: MOE_BUCKET_ROUTE_US.swap(0, Ordering::Relaxed),
111 plan_us: MOE_BUCKET_PLAN_US.swap(0, Ordering::Relaxed),
112 gather_us: MOE_BUCKET_GATHER_US.swap(0, Ordering::Relaxed),
113 gemm1_us: MOE_BUCKET_GEMM1_US.swap(0, Ordering::Relaxed),
114 silu_us: MOE_BUCKET_SILU_US.swap(0, Ordering::Relaxed),
115 gemm3_us: MOE_BUCKET_GEMM3_US.swap(0, Ordering::Relaxed),
116 combine_us: MOE_BUCKET_COMBINE_US.swap(0, Ordering::Relaxed),
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
121struct MoeDispatchRuntimeConfig {
122 moe_profile: bool,
123 decode_op_profile: bool,
124 vllm_moe_zero_ws: bool,
125 vllm_moe_pair_ids: bool,
126 moe_load_trace: bool,
127 moe_block_size: Option<usize>,
128 moe_large_m_block_size: Option<usize>,
129 moe_large_m_min_pairs: usize,
130 vllm_moe: bool,
131 moe_host_route: bool,
132}
133
134impl Default for MoeDispatchRuntimeConfig {
135 fn default() -> Self {
136 Self {
137 moe_profile: false,
138 decode_op_profile: false,
139 vllm_moe_zero_ws: false,
140 vllm_moe_pair_ids: false,
141 moe_load_trace: false,
142 moe_block_size: None,
143 moe_large_m_block_size: None,
144 moe_large_m_min_pairs: 1024,
145 vllm_moe: false,
146 moe_host_route: false,
147 }
148 }
149}
150
151impl MoeDispatchRuntimeConfig {
152 fn from_env() -> Self {
153 Self::from_env_vars(std::env::vars())
154 }
155
156 fn from_env_vars<I, K, V>(vars: I) -> Self
157 where
158 I: IntoIterator<Item = (K, V)>,
159 K: AsRef<str>,
160 V: AsRef<str>,
161 {
162 let mut config = Self::default();
163 for (name, value) in vars {
164 let value = value.as_ref();
165 match name.as_ref() {
166 "FERRUM_MOE_PROFILE" => config.moe_profile = true,
167 "FERRUM_DECODE_OP_PROFILE" => config.decode_op_profile = true,
168 "FERRUM_VLLM_MOE_ZERO_WS" => config.vllm_moe_zero_ws = value == "1",
169 "FERRUM_VLLM_MOE_PAIR_IDS" => config.vllm_moe_pair_ids = value == "1",
170 "FERRUM_MOE_LOAD_TRACE" => config.moe_load_trace = true,
171 "FERRUM_MOE_BLOCK_SIZE" => {
172 config.moe_block_size = parse_moe_block_size_value(value);
173 }
174 "FERRUM_MOE_LARGE_M_BLOCK_SIZE" => {
175 config.moe_large_m_block_size = parse_moe_block_size_value(value);
176 }
177 "FERRUM_MOE_LARGE_M_MIN_PAIRS" => {
178 config.moe_large_m_min_pairs = value.parse::<usize>().unwrap_or(1024);
179 }
180 "FERRUM_VLLM_MOE" => config.vllm_moe = value == "1",
181 "FERRUM_MOE_HOST_ROUTE" => config.moe_host_route = value == "1",
182 _ => {}
183 }
184 }
185 config
186 }
187}
188
189fn parse_moe_block_size_value(value: &str) -> Option<usize> {
190 value
191 .parse::<usize>()
192 .ok()
193 .filter(|bs| matches!(*bs, 8 | 16 | 32 | 48 | 64))
194}
195
196fn moe_dispatch_runtime_config() -> &'static MoeDispatchRuntimeConfig {
197 static CONFIG: OnceLock<MoeDispatchRuntimeConfig> = OnceLock::new();
198 CONFIG.get_or_init(MoeDispatchRuntimeConfig::from_env)
199}
200
201fn moe_profile_enabled() -> bool {
202 moe_dispatch_runtime_config().moe_profile
203}
204
205pub struct ExpertStack<B: QuantLlmBackend + BackendMoeFused> {
215 pub gate_up: Vec<Box<dyn Linear<B>>>,
218 pub down: Vec<Box<dyn Linear<B>>>,
220 pub gate_stacked: Option<Box<dyn StackedExpertGgufLinear<B>>>,
233 pub up_stacked: Option<Box<dyn StackedExpertGgufLinear<B>>>,
234 pub down_stacked: Option<Box<dyn StackedExpertGgufLinear<B>>>,
235
236 pub gate_up_marlin_stack: Option<std::sync::Arc<dyn ferrum_kernels::MarlinExpertStack<B>>>,
245 pub down_marlin_stack: Option<std::sync::Arc<dyn ferrum_kernels::MarlinExpertStack<B>>>,
246}
247
248impl<B: QuantLlmBackend + BackendMoeFused> ExpertStack<B> {
249 pub fn gate_up_stacked_store(
253 &self,
254 _expert_idx: usize,
255 ) -> Option<&std::sync::Arc<dyn ferrum_kernels::MarlinExpertStack<B>>> {
256 self.gate_up_marlin_stack.as_ref()
257 }
258
259 pub fn down_stacked_store(
261 &self,
262 _expert_idx: usize,
263 ) -> Option<&std::sync::Arc<dyn ferrum_kernels::MarlinExpertStack<B>>> {
264 self.down_marlin_stack.as_ref()
265 }
266
267 pub fn gemv_gate(
278 &self,
279 ctx: &mut B::Context,
280 input: &B::Buffer,
281 ids: &B::Buffer,
282 out: &mut B::Buffer,
283 top_k: usize,
284 ) -> Result<()> {
285 let weight = self.gate_stacked.as_deref().ok_or_else(|| {
286 FerrumError::unsupported("ExpertStack::gemv_gate: gate_stacked not loaded")
287 })?;
288 weight.gemv_moe_id(ctx, input, ids, out, top_k, 0)
289 }
290
291 pub fn gemv_up(
293 &self,
294 ctx: &mut B::Context,
295 input: &B::Buffer,
296 ids: &B::Buffer,
297 out: &mut B::Buffer,
298 top_k: usize,
299 ) -> Result<()> {
300 let weight = self.up_stacked.as_deref().ok_or_else(|| {
301 FerrumError::unsupported("ExpertStack::gemv_up: up_stacked not loaded")
302 })?;
303 weight.gemv_moe_id(ctx, input, ids, out, top_k, 0)
304 }
305
306 pub fn gemv_down(
309 &self,
310 ctx: &mut B::Context,
311 input: &B::Buffer,
312 ids: &B::Buffer,
313 out: &mut B::Buffer,
314 top_k: usize,
315 expert_intermediate: usize,
316 ) -> Result<()> {
317 let weight = self.down_stacked.as_deref().ok_or_else(|| {
318 FerrumError::unsupported("ExpertStack::gemv_down: down_stacked not loaded")
319 })?;
320 weight.gemv_moe_id(ctx, input, ids, out, top_k, expert_intermediate)
321 }
322
323 pub fn gemv_gate_up_silu_fused(
327 &self,
328 ctx: &mut B::Context,
329 input: &B::Buffer,
330 ids: &B::Buffer,
331 out_silu_stacked: &mut B::Buffer,
332 top_k: usize,
333 ) -> Result<()> {
334 let gate = self.gate_stacked.as_deref().ok_or_else(|| {
335 FerrumError::unsupported(
336 "ExpertStack::gemv_gate_up_silu_fused: gate_stacked not loaded",
337 )
338 })?;
339 let up = self.up_stacked.as_deref().ok_or_else(|| {
340 FerrumError::unsupported("ExpertStack::gemv_gate_up_silu_fused: up_stacked not loaded")
341 })?;
342 gate.gemv_moe_id_gate_up_silu(ctx, input, up, ids, out_silu_stacked, top_k)
343 }
344
345 #[allow(clippy::too_many_arguments)]
359 pub fn gemm_gate(
360 &self,
361 ctx: &mut B::Context,
362 src1: &B::Buffer,
363 ids: &B::Buffer,
364 tpe: &B::Buffer,
365 dst: &mut B::Buffer,
366 args_buf: Option<&B::Buffer>,
367 top_k: usize,
368 max_per_expert: usize,
369 tokens: usize,
370 ) -> Result<()> {
371 let weight = self.gate_stacked.as_deref().ok_or_else(|| {
372 FerrumError::unsupported("ExpertStack::gemm_gate: gate_stacked not loaded")
373 })?;
374 match args_buf {
375 Some(args) => weight.gemm_moe_id_indirect(
376 ctx,
377 src1,
378 ids,
379 tpe,
380 dst,
381 args,
382 1,
383 top_k,
384 max_per_expert,
385 tokens,
386 ),
387 None => weight.gemm_moe_id(ctx, src1, ids, tpe, dst, 1, top_k, max_per_expert, tokens),
388 }
389 }
390
391 #[allow(clippy::too_many_arguments)]
393 pub fn gemm_up(
394 &self,
395 ctx: &mut B::Context,
396 src1: &B::Buffer,
397 ids: &B::Buffer,
398 tpe: &B::Buffer,
399 dst: &mut B::Buffer,
400 args_buf: Option<&B::Buffer>,
401 top_k: usize,
402 max_per_expert: usize,
403 tokens: usize,
404 ) -> Result<()> {
405 let weight = self.up_stacked.as_deref().ok_or_else(|| {
406 FerrumError::unsupported("ExpertStack::gemm_up: up_stacked not loaded")
407 })?;
408 match args_buf {
409 Some(args) => weight.gemm_moe_id_indirect(
410 ctx,
411 src1,
412 ids,
413 tpe,
414 dst,
415 args,
416 1,
417 top_k,
418 max_per_expert,
419 tokens,
420 ),
421 None => weight.gemm_moe_id(ctx, src1, ids, tpe, dst, 1, top_k, max_per_expert, tokens),
422 }
423 }
424
425 #[allow(clippy::too_many_arguments)]
428 pub fn gemm_down(
429 &self,
430 ctx: &mut B::Context,
431 src1: &B::Buffer,
432 ids: &B::Buffer,
433 tpe: &B::Buffer,
434 dst: &mut B::Buffer,
435 args_buf: Option<&B::Buffer>,
436 top_k: usize,
437 max_per_expert: usize,
438 tokens: usize,
439 ) -> Result<()> {
440 let weight = self.down_stacked.as_deref().ok_or_else(|| {
441 FerrumError::unsupported("ExpertStack::gemm_down: down_stacked not loaded")
442 })?;
443 match args_buf {
444 Some(args) => weight.gemm_moe_id_indirect(
445 ctx,
446 src1,
447 ids,
448 tpe,
449 dst,
450 args,
451 top_k,
452 top_k,
453 max_per_expert,
454 tokens,
455 ),
456 None => weight.gemm_moe_id(
457 ctx,
458 src1,
459 ids,
460 tpe,
461 dst,
462 top_k,
463 top_k,
464 max_per_expert,
465 tokens,
466 ),
467 }
468 }
469
470 #[allow(clippy::too_many_arguments)]
478 pub fn gemv_gate_batched(
479 &self,
480 ctx: &mut B::Context,
481 input: &B::Buffer,
482 ids: &B::Buffer,
483 dst: &mut B::Buffer,
484 m: usize,
485 top_k: usize,
486 src1_outer_stride: usize,
487 src1_inner_stride: usize,
488 ) -> Result<()> {
489 let weight = self.gate_stacked.as_deref().ok_or_else(|| {
490 FerrumError::unsupported("ExpertStack::gemv_gate_batched: gate_stacked not loaded")
491 })?;
492 weight.gemv_moe_id_batched(
493 ctx,
494 input,
495 ids,
496 dst,
497 m,
498 top_k,
499 src1_outer_stride,
500 src1_inner_stride,
501 )
502 }
503
504 #[allow(clippy::too_many_arguments)]
506 pub fn gemv_up_batched(
507 &self,
508 ctx: &mut B::Context,
509 input: &B::Buffer,
510 ids: &B::Buffer,
511 dst: &mut B::Buffer,
512 m: usize,
513 top_k: usize,
514 src1_outer_stride: usize,
515 src1_inner_stride: usize,
516 ) -> Result<()> {
517 let weight = self.up_stacked.as_deref().ok_or_else(|| {
518 FerrumError::unsupported("ExpertStack::gemv_up_batched: up_stacked not loaded")
519 })?;
520 weight.gemv_moe_id_batched(
521 ctx,
522 input,
523 ids,
524 dst,
525 m,
526 top_k,
527 src1_outer_stride,
528 src1_inner_stride,
529 )
530 }
531
532 #[allow(clippy::too_many_arguments)]
534 pub fn gemv_down_batched(
535 &self,
536 ctx: &mut B::Context,
537 input: &B::Buffer,
538 ids: &B::Buffer,
539 dst: &mut B::Buffer,
540 m: usize,
541 top_k: usize,
542 src1_outer_stride: usize,
543 src1_inner_stride: usize,
544 ) -> Result<()> {
545 let weight = self.down_stacked.as_deref().ok_or_else(|| {
546 FerrumError::unsupported("ExpertStack::gemv_down_batched: down_stacked not loaded")
547 })?;
548 weight.gemv_moe_id_batched(
549 ctx,
550 input,
551 ids,
552 dst,
553 m,
554 top_k,
555 src1_outer_stride,
556 src1_inner_stride,
557 )
558 }
559
560 #[allow(clippy::too_many_arguments)]
563 pub fn gemv_gate_up_silu_batched_fused(
564 &self,
565 ctx: &mut B::Context,
566 input: &B::Buffer,
567 ids: &B::Buffer,
568 silu_out: &mut B::Buffer,
569 m: usize,
570 top_k: usize,
571 src1_outer_stride: usize,
572 src1_inner_stride: usize,
573 ) -> Result<()> {
574 let gate = self.gate_stacked.as_deref().ok_or_else(|| {
575 FerrumError::unsupported(
576 "ExpertStack::gemv_gate_up_silu_batched_fused: gate_stacked not loaded",
577 )
578 })?;
579 let up = self.up_stacked.as_deref().ok_or_else(|| {
580 FerrumError::unsupported(
581 "ExpertStack::gemv_gate_up_silu_batched_fused: up_stacked not loaded",
582 )
583 })?;
584 gate.gemv_moe_id_gate_up_silu_batched(
585 ctx,
586 input,
587 up,
588 ids,
589 silu_out,
590 m,
591 top_k,
592 src1_outer_stride,
593 src1_inner_stride,
594 )
595 }
596
597 #[allow(clippy::too_many_arguments)]
605 pub fn gemv_gate_offset(
606 &self,
607 ctx: &mut B::Context,
608 src1: &B::Buffer,
609 src1_offset: usize,
610 ids: &B::Buffer,
611 ids_offset: usize,
612 dst: &mut B::Buffer,
613 top_k: usize,
614 src1_stride: usize,
615 ) -> Result<()> {
616 let weight = self.gate_stacked.as_deref().ok_or_else(|| {
617 FerrumError::unsupported("ExpertStack::gemv_gate_offset: gate_stacked not loaded")
618 })?;
619 weight.gemv_moe_id_offset(
620 ctx,
621 src1,
622 src1_offset,
623 ids,
624 ids_offset,
625 dst,
626 top_k,
627 src1_stride,
628 )
629 }
630
631 #[allow(clippy::too_many_arguments)]
633 pub fn gemv_up_offset(
634 &self,
635 ctx: &mut B::Context,
636 src1: &B::Buffer,
637 src1_offset: usize,
638 ids: &B::Buffer,
639 ids_offset: usize,
640 dst: &mut B::Buffer,
641 top_k: usize,
642 src1_stride: usize,
643 ) -> Result<()> {
644 let weight = self.up_stacked.as_deref().ok_or_else(|| {
645 FerrumError::unsupported("ExpertStack::gemv_up_offset: up_stacked not loaded")
646 })?;
647 weight.gemv_moe_id_offset(
648 ctx,
649 src1,
650 src1_offset,
651 ids,
652 ids_offset,
653 dst,
654 top_k,
655 src1_stride,
656 )
657 }
658
659 #[allow(clippy::too_many_arguments)]
661 pub fn gemv_down_offset(
662 &self,
663 ctx: &mut B::Context,
664 src1: &B::Buffer,
665 src1_offset: usize,
666 ids: &B::Buffer,
667 ids_offset: usize,
668 dst: &mut B::Buffer,
669 top_k: usize,
670 src1_stride: usize,
671 ) -> Result<()> {
672 let weight = self.down_stacked.as_deref().ok_or_else(|| {
673 FerrumError::unsupported("ExpertStack::gemv_down_offset: down_stacked not loaded")
674 })?;
675 weight.gemv_moe_id_offset(
676 ctx,
677 src1,
678 src1_offset,
679 ids,
680 ids_offset,
681 dst,
682 top_k,
683 src1_stride,
684 )
685 }
686}
687
688impl<B: QuantLlmBackend + BackendMoeFused> ExpertStack<B> {
689 pub fn from_dense_stacks(
696 gate_stack: &[f32],
697 up_stack: &[f32],
698 down_stack: &[f32],
699 num_experts: usize,
700 hidden_size: usize,
701 expert_intermediate: usize,
702 ) -> Result<Self> {
703 let gate_up_per_expert = expert_intermediate * hidden_size;
704 let down_per_expert = hidden_size * expert_intermediate;
705
706 check_size(
707 gate_stack.len(),
708 num_experts * gate_up_per_expert,
709 "gate_stack",
710 )?;
711 check_size(up_stack.len(), num_experts * gate_up_per_expert, "up_stack")?;
712 check_size(
713 down_stack.len(),
714 num_experts * down_per_expert,
715 "down_stack",
716 )?;
717
718 let mut gate_up = Vec::with_capacity(num_experts);
719 let mut down = Vec::with_capacity(num_experts);
720 for e in 0..num_experts {
721 let g_off = e * gate_up_per_expert;
722 let g_slice = &gate_stack[g_off..g_off + gate_up_per_expert];
723 let u_slice = &up_stack[g_off..g_off + gate_up_per_expert];
724
725 let mut fused = Vec::with_capacity(2 * gate_up_per_expert);
730 fused.extend_from_slice(g_slice);
731 fused.extend_from_slice(u_slice);
732 gate_up.push(Box::new(DenseLinear::<B>::from_rows(
733 &fused,
734 2 * expert_intermediate,
735 hidden_size,
736 )) as Box<dyn Linear<B>>);
737
738 let d_off = e * down_per_expert;
739 let d_slice = &down_stack[d_off..d_off + down_per_expert];
740 down.push(Box::new(DenseLinear::<B>::from_rows(
741 d_slice,
742 hidden_size,
743 expert_intermediate,
744 )) as Box<dyn Linear<B>>);
745 }
746 Ok(Self {
747 gate_up,
748 down,
749 gate_stacked: None,
750 up_stacked: None,
751 down_stacked: None,
752 gate_up_marlin_stack: None,
753 down_marlin_stack: None,
754 })
755 }
756
757 pub fn load_from_gguf(
777 gguf: &GgufFile,
778 layer_idx: usize,
779 num_experts: usize,
780 hidden_size: usize,
781 expert_intermediate: usize,
782 ) -> Result<Self> {
783 let runtime_config = moe_dispatch_runtime_config();
784 if let Some(quant) = Self::try_load_quantised(
785 gguf,
786 layer_idx,
787 num_experts,
788 hidden_size,
789 expert_intermediate,
790 )? {
791 if runtime_config.moe_load_trace {
792 eprintln!("[moe-load] layer {layer_idx} → quantised expert path");
793 }
794 return Ok(quant);
795 }
796
797 if runtime_config.moe_load_trace {
798 eprintln!("[moe-load] layer {layer_idx} → eager fp32 dense fallback ⚠");
799 }
800
801 let device = Device::Cpu;
802 let gate = read_dequant_flat(
803 gguf,
804 &format!("blk.{layer_idx}.ffn_gate_exps.weight"),
805 &device,
806 )?;
807 let up = read_dequant_flat(
808 gguf,
809 &format!("blk.{layer_idx}.ffn_up_exps.weight"),
810 &device,
811 )?;
812 let down = read_dequant_flat(
813 gguf,
814 &format!("blk.{layer_idx}.ffn_down_exps.weight"),
815 &device,
816 )?;
817 Self::from_dense_stacks(
820 &gate,
821 &up,
822 &down,
823 num_experts,
824 hidden_size,
825 expert_intermediate,
826 )
827 }
828
829 fn try_load_quantised(
835 gguf: &GgufFile,
836 layer_idx: usize,
837 num_experts: usize,
838 hidden_size: usize,
839 expert_intermediate: usize,
840 ) -> Result<Option<Self>> {
841 let device = Device::Cpu;
842
843 let gate_name = format!("blk.{layer_idx}.ffn_gate_exps.weight");
844 let up_name = format!("blk.{layer_idx}.ffn_up_exps.weight");
845 let down_name = format!("blk.{layer_idx}.ffn_down_exps.weight");
846
847 let gate_kind = match quant_kind(gguf, &gate_name)? {
851 Some(k) => k,
852 None => return Ok(None),
853 };
854 let up_kind = match quant_kind(gguf, &up_name)? {
855 Some(k) => k,
856 None => return Ok(None),
857 };
858 let down_kind = match quant_kind(gguf, &down_name)? {
859 Some(k) => k,
860 None => return Ok(None),
861 };
862
863 let gate_bytes = gguf.tensor_byte_slice(&gate_name).ok_or_else(|| {
873 FerrumError::model(format!("MoE: tensor_byte_slice failed for '{gate_name}'"))
874 })?;
875 let up_bytes = gguf.tensor_byte_slice(&up_name).ok_or_else(|| {
876 FerrumError::model(format!("MoE: tensor_byte_slice failed for '{up_name}'"))
877 })?;
878 let down_bytes = gguf.tensor_byte_slice(&down_name).ok_or_else(|| {
879 FerrumError::model(format!("MoE: tensor_byte_slice failed for '{down_name}'"))
880 })?;
881 let _ = device; let gate_per = block_bytes_for(
887 gate_kind,
888 expert_intermediate * hidden_size,
889 "ffn_gate_exps",
890 )?;
891 let up_per = block_bytes_for(up_kind, expert_intermediate * hidden_size, "ffn_up_exps")?;
892 let down_per = block_bytes_for(
893 down_kind,
894 hidden_size * expert_intermediate,
895 "ffn_down_exps",
896 )?;
897
898 check_size(
899 gate_bytes.len(),
900 num_experts * gate_per,
901 "ffn_gate_exps bytes",
902 )?;
903 check_size(up_bytes.len(), num_experts * up_per, "ffn_up_exps bytes")?;
904 check_size(
905 down_bytes.len(),
906 num_experts * down_per,
907 "ffn_down_exps bytes",
908 )?;
909
910 let gate_stacked = B::load_quant_experts(
918 gate_kind,
919 gate_bytes,
920 num_experts,
921 expert_intermediate,
922 hidden_size,
923 )
924 .ok();
925 let up_stacked = B::load_quant_experts(
926 up_kind,
927 up_bytes,
928 num_experts,
929 expert_intermediate,
930 hidden_size,
931 )
932 .ok();
933 let down_stacked = B::load_quant_experts(
934 down_kind,
935 down_bytes,
936 num_experts,
937 hidden_size,
938 expert_intermediate,
939 )
940 .ok();
941
942 let stacked_complete =
950 gate_stacked.is_some() && up_stacked.is_some() && down_stacked.is_some();
951
952 let (gate_up, down) = if stacked_complete {
953 (Vec::new(), Vec::new())
956 } else {
957 let mut gate_up: Vec<Box<dyn Linear<B>>> = Vec::with_capacity(num_experts);
958 let mut down: Vec<Box<dyn Linear<B>>> = Vec::with_capacity(num_experts);
959 for e in 0..num_experts {
960 let g_slice = &gate_bytes[e * gate_per..(e + 1) * gate_per];
961 let u_slice = &up_bytes[e * up_per..(e + 1) * up_per];
962 let d_slice = &down_bytes[e * down_per..(e + 1) * down_per];
963
964 let parts: [(GgufQuantType, &[u8], usize); 2] = [
965 (gate_kind, g_slice, expert_intermediate),
966 (up_kind, u_slice, expert_intermediate),
967 ];
968 let gate_up_e = match QuantLinear::<B>::from_gguf_fused(&parts, hidden_size) {
969 Ok(q) => q,
970 Err(_) => return Ok(None),
971 };
972 gate_up.push(Box::new(gate_up_e) as Box<dyn Linear<B>>);
973
974 let down_e = match QuantLinear::<B>::from_gguf_bytes(
975 down_kind,
976 d_slice,
977 hidden_size,
978 expert_intermediate,
979 ) {
980 Ok(q) => q,
981 Err(_) => return Ok(None),
982 };
983 down.push(Box::new(down_e) as Box<dyn Linear<B>>);
984 }
985 (gate_up, down)
986 };
987
988 Ok(Some(Self {
989 gate_up,
990 down,
991 gate_stacked,
992 up_stacked,
993 down_stacked,
994 gate_up_marlin_stack: None,
995 down_marlin_stack: None,
996 }))
997 }
998
999 pub fn open_and_load(
1003 path: impl AsRef<Path>,
1004 layer_idx: usize,
1005 num_experts: usize,
1006 hidden_size: usize,
1007 expert_intermediate: usize,
1008 ) -> Result<Self> {
1009 let gguf = GgufFile::open(path).map_err(candle_to_ferrum)?;
1010 Self::load_from_gguf(
1011 &gguf,
1012 layer_idx,
1013 num_experts,
1014 hidden_size,
1015 expert_intermediate,
1016 )
1017 }
1018
1019 pub fn num_experts(&self) -> usize {
1027 debug_assert_eq!(
1028 self.gate_up.len(),
1029 self.down.len(),
1030 "ExpertStack: gate_up and down disagree on expert count"
1031 );
1032 if !self.gate_up.is_empty() || !self.down.is_empty() {
1033 return self.gate_up.len();
1034 }
1035 if let Some(gate) = self.gate_stacked.as_deref() {
1036 let num_experts = gate.num_experts();
1037 debug_assert_eq!(
1038 self.up_stacked.as_deref().map(|up| up.num_experts()),
1039 Some(num_experts),
1040 "ExpertStack: gate/up stacked expert counts disagree"
1041 );
1042 debug_assert_eq!(
1043 self.down_stacked.as_deref().map(|down| down.num_experts()),
1044 Some(num_experts),
1045 "ExpertStack: gate/down stacked expert counts disagree"
1046 );
1047 return num_experts;
1048 }
1049 if let Some(gate_up) = self.gate_up_marlin_stack.as_deref() {
1050 let num_experts = gate_up.num_experts();
1051 debug_assert_eq!(
1052 self.down_marlin_stack
1053 .as_deref()
1054 .map(|down| down.num_experts()),
1055 Some(num_experts),
1056 "ExpertStack: gate_up/down Marlin expert counts disagree"
1057 );
1058 return num_experts;
1059 }
1060 0
1061 }
1062}
1063
1064pub struct MoeForwardParams<'a, B: QuantLlmBackend + BackendMoeFused> {
1102 pub ctx: &'a mut B::Context,
1103 pub x: &'a B::Buffer,
1104 pub router_logits: &'a B::Buffer,
1105 pub out: &'a mut B::Buffer,
1106 pub batch: usize,
1107 pub hidden_size: usize,
1108 pub expert_intermediate: usize,
1109 pub num_experts: usize,
1110 pub top_k: usize,
1111 pub norm_topk_prob: bool,
1112 pub experts: &'a ExpertStack<B>,
1113 pub x_single: &'a mut B::Buffer,
1114 pub acc_buf: &'a mut B::Buffer,
1115 pub gate_up_buf: &'a mut B::Buffer,
1116 pub silu_buf: &'a mut B::Buffer,
1117 pub down_buf: &'a mut B::Buffer,
1118 pub zero_hidden: &'a B::Buffer,
1119}
1120
1121pub fn moe_forward<B: QuantLlmBackend + BackendMoeFused>(
1122 params: MoeForwardParams<'_, B>,
1123) -> Result<()> {
1124 let MoeForwardParams {
1125 ctx,
1126 x,
1127 router_logits,
1128 out,
1129 batch,
1130 hidden_size,
1131 expert_intermediate,
1132 num_experts,
1133 top_k,
1134 norm_topk_prob,
1135 experts,
1136 x_single,
1137 acc_buf,
1138 gate_up_buf,
1139 silu_buf,
1140 down_buf,
1141 zero_hidden,
1142 } = params;
1143 let n_experts = experts.num_experts();
1144 if n_experts != num_experts {
1145 return Err(FerrumError::model(format!(
1146 "moe_forward: experts.num_experts() = {n_experts} != cfg.num_experts = {num_experts}"
1147 )));
1148 }
1149
1150 let prof = moe_profile_enabled();
1151
1152 let t0 = if prof {
1156 Some(std::time::Instant::now())
1157 } else {
1158 None
1159 };
1160 B::sync(ctx);
1161 if let Some(t) = t0 {
1162 MOE_SYNC_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
1163 MOE_SYNC_CALLS.fetch_add(1, Ordering::Relaxed);
1164 }
1165
1166 let t0 = if prof {
1167 Some(std::time::Instant::now())
1168 } else {
1169 None
1170 };
1171 let logits_host = B::to_vec(router_logits, batch * num_experts);
1172 let route_out =
1173 crate::moe::router::route(&logits_host, batch, num_experts, top_k, norm_topk_prob);
1174 if let Some(t) = t0 {
1175 MOE_HOST_TOPK_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
1176 MOE_HOST_TOPK_CALLS.fetch_add(1, Ordering::Relaxed);
1177 }
1178
1179 for b in 0..batch {
1180 let t0 = if prof {
1182 Some(std::time::Instant::now())
1183 } else {
1184 None
1185 };
1186 B::copy_slice(ctx, x, b * hidden_size, x_single, 0, hidden_size);
1187 B::copy_slice(ctx, zero_hidden, 0, acc_buf, 0, hidden_size);
1188 if let Some(t) = t0 {
1189 MOE_COPY_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
1190 MOE_COPY_CALLS.fetch_add(2, Ordering::Relaxed);
1191 }
1192
1193 for k in 0..top_k {
1194 let pair = b * top_k + k;
1195 let expert_id = route_out.expert_ids[pair] as usize;
1196 let weight = route_out.expert_weights[pair];
1197 if expert_id >= num_experts {
1198 return Err(FerrumError::model(format!(
1199 "moe_forward: routed expert {expert_id} >= num_experts {num_experts}"
1200 )));
1201 }
1202
1203 let t0 = if prof {
1205 B::sync(ctx);
1206 Some(std::time::Instant::now())
1207 } else {
1208 None
1209 };
1210 experts.gate_up[expert_id].forward(ctx, x_single, gate_up_buf, 1);
1211 if let Some(t) = t0 {
1212 B::sync(ctx);
1213 MOE_GEMV_GATE_UP_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
1214 MOE_GEMV_GATE_UP_CALLS.fetch_add(1, Ordering::Relaxed);
1215 }
1216
1217 let t0 = if prof {
1219 Some(std::time::Instant::now())
1220 } else {
1221 None
1222 };
1223 B::fused_silu_mul_split(ctx, gate_up_buf, silu_buf, 1, expert_intermediate);
1224 if let Some(t) = t0 {
1225 B::sync(ctx);
1226 MOE_SILU_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
1227 MOE_SILU_CALLS.fetch_add(1, Ordering::Relaxed);
1228 }
1229
1230 let t0 = if prof {
1232 Some(std::time::Instant::now())
1233 } else {
1234 None
1235 };
1236 experts.down[expert_id].forward(ctx, silu_buf, down_buf, 1);
1237 if let Some(t) = t0 {
1238 B::sync(ctx);
1239 MOE_GEMV_DOWN_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
1240 MOE_GEMV_DOWN_CALLS.fetch_add(1, Ordering::Relaxed);
1241 }
1242
1243 let t0 = if prof {
1245 Some(std::time::Instant::now())
1246 } else {
1247 None
1248 };
1249 B::scaled_add_inplace(ctx, acc_buf, down_buf, weight, hidden_size);
1250 if let Some(t) = t0 {
1251 B::sync(ctx);
1252 MOE_SCALED_ADD_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
1253 MOE_SCALED_ADD_CALLS.fetch_add(1, Ordering::Relaxed);
1254 }
1255 }
1256
1257 let t0 = if prof {
1259 Some(std::time::Instant::now())
1260 } else {
1261 None
1262 };
1263 B::copy_slice(ctx, acc_buf, 0, out, b * hidden_size, hidden_size);
1264 if let Some(t) = t0 {
1265 MOE_COPY_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
1266 MOE_COPY_CALLS.fetch_add(1, Ordering::Relaxed);
1267 }
1268 }
1269
1270 Ok(())
1271}
1272
1273pub const MOE_BLOCK_SIZE_MAX: usize = 64;
1276
1277fn pick_moe_block_size(
1292 plan: Option<&MoeBucketPlan>,
1293 num_experts: usize,
1294 use_device_route: bool,
1295 total_pairs: usize,
1296) -> usize {
1297 pick_moe_block_size_with_config(
1298 moe_dispatch_runtime_config(),
1299 plan,
1300 num_experts,
1301 use_device_route,
1302 total_pairs,
1303 )
1304}
1305
1306fn pick_moe_block_size_with_config(
1307 config: &MoeDispatchRuntimeConfig,
1308 plan: Option<&MoeBucketPlan>,
1309 num_experts: usize,
1310 use_device_route: bool,
1311 total_pairs: usize,
1312) -> usize {
1313 const CANDIDATES: &[usize] = &[64, 32, 16];
1314 const PADDING_BUDGET: f64 = 1.30; if let Some(bs) = config.moe_block_size {
1319 return bs;
1320 }
1321 if use_device_route {
1322 if let Some(bs) = config.moe_large_m_block_size {
1323 if total_pairs >= config.moe_large_m_min_pairs {
1324 return bs;
1325 }
1326 }
1327 return 16;
1336 }
1337 let Some(plan) = plan else {
1338 return 16;
1339 };
1340 let m_e: Vec<usize> = (0..num_experts)
1341 .map(|e| plan.expert_offsets[e + 1] - plan.expert_offsets[e])
1342 .collect();
1343 let total_actual: usize = m_e.iter().sum();
1344 if total_actual == 0 {
1345 return 16;
1346 }
1347 for &bs in CANDIDATES {
1348 let total_padded: usize = m_e.iter().map(|&m| m.div_ceil(bs) * bs).sum();
1349 if (total_padded as f64) <= (total_actual as f64) * PADDING_BUDGET {
1350 return bs;
1351 }
1352 }
1353 16
1354}
1355
1356pub struct MoeBucketPlan {
1361 pub expert_offsets: Vec<usize>,
1365 pub packed_token_idx: Vec<u32>,
1368 pub pairs_by_token: Vec<i32>,
1372 pub pair_weights: Vec<f32>,
1376 cursors: Vec<usize>,
1380}
1381
1382impl MoeBucketPlan {
1383 pub fn empty() -> Self {
1387 Self {
1388 expert_offsets: Vec::new(),
1389 packed_token_idx: Vec::new(),
1390 pairs_by_token: Vec::new(),
1391 pair_weights: Vec::new(),
1392 cursors: Vec::new(),
1393 }
1394 }
1395
1396 pub fn build(route: &RouterOutput, batch: usize, num_experts: usize, top_k: usize) -> Self {
1399 let mut p = Self::empty();
1400 p.rebuild_into(route, batch, num_experts, top_k);
1401 p
1402 }
1403
1404 pub fn rebuild_into(
1409 &mut self,
1410 route: &RouterOutput,
1411 batch: usize,
1412 num_experts: usize,
1413 top_k: usize,
1414 ) {
1415 debug_assert_eq!(route.expert_ids.len(), batch * top_k);
1416 debug_assert_eq!(route.expert_weights.len(), batch * top_k);
1417 let total_pairs = batch * top_k;
1418
1419 self.expert_offsets.clear();
1420 self.expert_offsets.resize(num_experts + 1, 0);
1421 self.packed_token_idx.clear();
1422 self.packed_token_idx.resize(total_pairs, 0);
1423 self.pairs_by_token.clear();
1424 self.pairs_by_token.resize(total_pairs, -1);
1425
1426 for &eid in &route.expert_ids {
1430 self.expert_offsets[eid as usize + 1] += 1;
1431 }
1432
1433 for e in 0..num_experts {
1435 self.expert_offsets[e + 1] += self.expert_offsets[e];
1436 }
1437
1438 self.cursors.clear();
1443 self.cursors
1444 .extend_from_slice(&self.expert_offsets[..num_experts]);
1445
1446 for b in 0..batch {
1447 for k in 0..top_k {
1448 let pair_flat = b * top_k + k;
1449 let eid = route.expert_ids[pair_flat] as usize;
1450 let slot = self.cursors[eid];
1451 self.cursors[eid] += 1;
1452 self.packed_token_idx[slot] = b as u32;
1453 self.pairs_by_token[pair_flat] = slot as i32;
1454 }
1455 }
1456
1457 self.pair_weights.clear();
1460 self.pair_weights.extend_from_slice(&route.expert_weights);
1461 }
1462}
1463
1464pub struct MoeRouteScratch {
1472 pub output: RouterOutput,
1473 pub probs: Vec<f32>,
1476 pub plan: MoeBucketPlan,
1477}
1478
1479impl MoeRouteScratch {
1480 pub fn new() -> Self {
1481 Self {
1482 output: RouterOutput::empty(),
1483 probs: Vec::new(),
1484 plan: MoeBucketPlan::empty(),
1485 }
1486 }
1487}
1488
1489impl Default for MoeRouteScratch {
1490 fn default() -> Self {
1491 Self::new()
1492 }
1493}
1494
1495pub struct DeviceRouteScratch<'a, B: crate::moe::dispatch::Backend> {
1504 pub selected_ids: &'a mut B::Buffer,
1505 pub pair_weights: &'a mut B::Buffer,
1506 pub pairs_by_token: &'a mut B::Buffer,
1507 pub packed_token_idx: &'a mut B::Buffer,
1508 pub expert_offsets: &'a mut B::Buffer,
1509 pub sorted_tokens: &'a mut B::Buffer,
1513 pub block_ids: &'a mut B::Buffer,
1514 pub total_post_pad: &'a mut B::Buffer,
1515}
1516
1517pub struct MoeForwardBucketedParams<'a, B: QuantLlmBackend + BackendMoeFused> {
1534 pub ctx: &'a mut B::Context,
1535 pub x: &'a B::Buffer,
1536 pub router_logits: &'a B::Buffer,
1537 pub out: &'a mut B::Buffer,
1538 pub batch: usize,
1539 pub hidden_size: usize,
1540 pub expert_intermediate: usize,
1541 pub num_experts: usize,
1542 pub top_k: usize,
1543 pub norm_topk_prob: bool,
1544 pub experts: &'a ExpertStack<B>,
1545 pub x_packed: &'a mut B::Buffer,
1546 pub gate_up_packed: &'a mut B::Buffer,
1547 pub silu_packed: &'a mut B::Buffer,
1548 pub down_packed: &'a mut B::Buffer,
1549 pub route_scratch: &'a mut MoeRouteScratch,
1550 pub profile_bucket: bool,
1551 pub device_route: Option<DeviceRouteScratch<'a, B>>,
1556}
1557
1558pub fn moe_forward_bucketed<B: QuantLlmBackend + BackendMoeFused>(
1559 params: MoeForwardBucketedParams<'_, B>,
1560) -> Result<()> {
1561 let MoeForwardBucketedParams {
1562 ctx,
1563 x,
1564 router_logits,
1565 out,
1566 batch,
1567 hidden_size,
1568 expert_intermediate,
1569 num_experts,
1570 top_k,
1571 norm_topk_prob,
1572 experts,
1573 x_packed,
1574 gate_up_packed,
1575 silu_packed,
1576 down_packed,
1577 route_scratch,
1578 profile_bucket,
1579 device_route,
1580 } = params;
1581 if experts.num_experts() != num_experts {
1582 return Err(FerrumError::model(format!(
1583 "moe_forward_bucketed: experts {} != num_experts {num_experts}",
1584 experts.num_experts()
1585 )));
1586 }
1587
1588 let runtime_config = moe_dispatch_runtime_config();
1589 let prof = profile_bucket || runtime_config.moe_profile || runtime_config.decode_op_profile;
1592 if prof {
1593 MOE_BUCKET_LAYER_CALLS.fetch_add(1, Ordering::Relaxed);
1594 }
1595
1596 let stack_requires_vllm = experts
1610 .gate_up_marlin_stack
1611 .as_ref()
1612 .is_some_and(|stack| stack.requires_vllm_moe())
1613 || experts
1614 .down_marlin_stack
1615 .as_ref()
1616 .is_some_and(|stack| stack.requires_vllm_moe());
1617 let use_vllm_moe = runtime_config.vllm_moe || stack_requires_vllm;
1618 let use_device_route = device_route.is_some() && use_vllm_moe && !runtime_config.moe_host_route;
1635 let use_vllm_pair_ids = use_device_route && runtime_config.vllm_moe_pair_ids;
1636
1637 let mut dr_kept: Option<DeviceRouteScratch<'_, B>> = if use_device_route {
1644 let dr = device_route.expect("device_route is Some when use_device_route");
1645 B::route_topk_softmax(
1646 ctx,
1647 router_logits,
1648 dr.selected_ids,
1649 dr.pair_weights,
1650 batch,
1651 num_experts,
1652 top_k,
1653 norm_topk_prob,
1654 )?;
1655 if !use_vllm_pair_ids {
1656 B::moe_build_pairs_by_token(
1657 ctx,
1658 dr.selected_ids,
1659 dr.pairs_by_token,
1660 dr.packed_token_idx,
1661 dr.expert_offsets,
1662 batch * top_k,
1663 num_experts,
1664 top_k,
1665 )?;
1666 }
1667 Some(dr)
1668 } else {
1669 None
1670 };
1671
1672 let plan: Option<&crate::moe::MoeBucketPlan> = if !use_device_route {
1684 let t_route_total = if prof {
1685 Some(std::time::Instant::now())
1686 } else {
1687 None
1688 };
1689 let gpu_route = B::try_gpu_route_topk_into_host(
1690 ctx,
1691 router_logits,
1692 &mut route_scratch.output.expert_ids,
1693 &mut route_scratch.output.expert_weights,
1694 batch,
1695 num_experts,
1696 top_k,
1697 norm_topk_prob,
1698 );
1699 if gpu_route.is_err() {
1700 let t_sync = if prof {
1701 Some(std::time::Instant::now())
1702 } else {
1703 None
1704 };
1705 B::sync(ctx);
1706 if let Some(t) = t_sync {
1707 MOE_BUCKET_SYNC_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
1708 }
1709 let t_d2h = if prof {
1710 Some(std::time::Instant::now())
1711 } else {
1712 None
1713 };
1714 let logits_host = B::to_vec(router_logits, batch * num_experts);
1715 if let Some(t) = t_d2h {
1716 MOE_BUCKET_D2H_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
1717 }
1718 let t_route = if prof {
1719 Some(std::time::Instant::now())
1720 } else {
1721 None
1722 };
1723 crate::moe::router::route_into(
1724 &logits_host,
1725 batch,
1726 num_experts,
1727 top_k,
1728 norm_topk_prob,
1729 &mut route_scratch.output,
1730 &mut route_scratch.probs,
1731 );
1732 if let Some(t) = t_route {
1733 MOE_BUCKET_ROUTE_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
1734 }
1735 } else if let Some(t) = t_route_total {
1736 MOE_BUCKET_ROUTE_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
1737 }
1738 let t_plan = if prof {
1739 Some(std::time::Instant::now())
1740 } else {
1741 None
1742 };
1743 route_scratch
1744 .plan
1745 .rebuild_into(&route_scratch.output, batch, num_experts, top_k);
1746 if let Some(t) = t_plan {
1747 MOE_BUCKET_PLAN_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
1748 }
1749 Some(&route_scratch.plan)
1750 } else {
1751 None
1752 };
1753
1754 if !use_vllm_pair_ids {
1758 let t_gather = if prof {
1759 Some(std::time::Instant::now())
1760 } else {
1761 None
1762 };
1763 if let Some(ref dr) = dr_kept {
1764 B::embedding_lookup_dev(
1765 ctx,
1766 x,
1767 dr.packed_token_idx,
1768 x_packed,
1769 batch * top_k,
1770 hidden_size,
1771 );
1772 } else {
1773 let plan = plan.expect("plan is Some when !use_device_route");
1774 B::embedding_lookup(ctx, x, &plan.packed_token_idx, x_packed, hidden_size);
1775 }
1776 if let Some(t) = t_gather {
1777 B::sync(ctx);
1778 MOE_BUCKET_GATHER_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
1779 }
1780 }
1781
1782 let gate_up_dim_per_expert = 2 * expert_intermediate;
1790 let down_n_per_expert = hidden_size;
1791 let gu_store = experts.gate_up_stacked_store(0).ok_or_else(|| {
1797 FerrumError::model(
1798 "moe_forward_bucketed requires stacked gate_up store \
1799 (load via Qwen3MoeModel::new_safetensors)",
1800 )
1801 })?;
1802 let zero_marlin_workspace = !use_vllm_moe || runtime_config.vllm_moe_zero_ws;
1803 if zero_marlin_workspace {
1804 let _ = gu_store.zero_workspace(ctx);
1805 }
1806
1807 let total_pairs_active = batch * top_k;
1813 let max_block_size: usize = 64;
1847 let moe_block_size: usize = pick_moe_block_size_with_config(
1848 runtime_config,
1849 plan,
1850 num_experts,
1851 use_device_route,
1852 total_pairs_active,
1853 );
1854 debug_assert!(
1855 moe_block_size <= max_block_size,
1856 "moe_block_size {moe_block_size} exceeds scratch worst-case {max_block_size}"
1857 );
1858 let sorted_max_size = batch * top_k + num_experts * moe_block_size;
1866 let vllm_routing_owned: Option<ferrum_kernels::backend::MoeRouting<B>> =
1867 if use_vllm_moe && !use_device_route {
1868 let plan = plan.expect("plan is Some when host vllm builder runs");
1869 let mut padded_offsets = Vec::with_capacity(num_experts + 1);
1870 let mut acc = 0usize;
1871 for e in 0..num_experts {
1872 padded_offsets.push(acc);
1873 let m_e = plan.expert_offsets[e + 1] - plan.expert_offsets[e];
1874 let pe = m_e.div_ceil(moe_block_size) * moe_block_size;
1875 acc += pe;
1876 }
1877 padded_offsets.push(acc);
1878 let total_padded = acc;
1879 let total_blocks = total_padded / moe_block_size;
1880 let sentinel = total_pairs_active as i32;
1881
1882 let mut sorted_token_ids = vec![sentinel; total_padded];
1883 let mut expert_ids = vec![0i32; total_blocks];
1884 for e in 0..num_experts {
1885 let m_e = plan.expert_offsets[e + 1] - plan.expert_offsets[e];
1886 if m_e == 0 {
1887 continue;
1888 }
1889 let p_off = padded_offsets[e];
1890 let real_off = plan.expert_offsets[e];
1891 for i in 0..m_e {
1892 sorted_token_ids[p_off + i] = (real_off + i) as i32;
1893 }
1894 let blocks_for_e = (padded_offsets[e + 1] - p_off) / moe_block_size;
1895 let block_start = p_off / moe_block_size;
1896 for b in 0..blocks_for_e {
1897 expert_ids[block_start + b] = e as i32;
1898 }
1899 }
1900 let num_tokens_past_padded = vec![total_padded as i32];
1901 Some(B::upload_moe_routing(
1902 ctx,
1903 &sorted_token_ids,
1904 &expert_ids,
1905 &num_tokens_past_padded,
1906 )?)
1907 } else {
1908 None
1909 };
1910
1911 if use_device_route {
1915 let dr = dr_kept
1916 .as_mut()
1917 .expect("dr_kept is Some when use_device_route");
1918 if use_vllm_pair_ids {
1919 B::moe_align_block_size_pair_ids(
1920 ctx,
1921 dr.selected_ids,
1922 dr.sorted_tokens,
1923 dr.block_ids,
1924 dr.total_post_pad,
1925 batch * top_k,
1926 num_experts,
1927 moe_block_size,
1928 sorted_max_size,
1929 )?;
1930 } else {
1931 B::moe_align_block_size(
1932 ctx,
1933 dr.selected_ids,
1934 dr.sorted_tokens,
1935 dr.block_ids,
1936 dr.total_post_pad,
1937 batch * top_k,
1938 num_experts,
1939 moe_block_size,
1940 sorted_max_size,
1941 )?;
1942 }
1943 }
1944
1945 let vllm_refs: Option<(&B::Buffer, &B::Buffer, &B::Buffer)> = if use_device_route {
1950 let dr = dr_kept
1951 .as_ref()
1952 .expect("dr_kept is Some when use_device_route");
1953 Some((&*dr.sorted_tokens, &*dr.block_ids, &*dr.total_post_pad))
1954 } else if let Some(r) = vllm_routing_owned.as_ref() {
1955 Some((
1956 &r.sorted_token_ids,
1957 &r.expert_ids,
1958 &r.num_tokens_past_padded,
1959 ))
1960 } else {
1961 None
1962 };
1963
1964 let phase1_dispatches: Vec<(usize, usize, usize, usize)> = if vllm_refs.is_none() {
1968 let plan = plan.expect("plan is Some when batched GEMM path runs");
1969 let mut v: Vec<(usize, usize, usize, usize)> = Vec::with_capacity(num_experts);
1970 for e in 0..num_experts {
1971 let m_e = plan.expert_offsets[e + 1] - plan.expert_offsets[e];
1972 if m_e == 0 {
1973 continue;
1974 }
1975 let pair_off = plan.expert_offsets[e];
1976 v.push((e, pair_off, pair_off, m_e));
1977 }
1978 v.sort_by(|a, b| b.3.cmp(&a.3).then_with(|| a.0.cmp(&b.0)));
1979 v
1980 } else {
1981 Vec::new()
1982 };
1983 let t_gemm1 = if prof {
1984 Some(std::time::Instant::now())
1985 } else {
1986 None
1987 };
1988 if let Some((sorted_tokens, block_ids, total_post_pad)) = vllm_refs {
1989 #[cfg(feature = "cuda")]
1991 let _marlin_label = if ferrum_kernels::backend::cuda::marlin::profile_marlin() {
1992 Some(ferrum_kernels::backend::cuda::push_alloc_label(
1993 "moe.vllm.gate_up_proj",
1994 ))
1995 } else {
1996 None
1997 };
1998 if use_vllm_pair_ids {
1999 gu_store.gemm_phase_vllm(
2000 ctx,
2001 x,
2002 sorted_tokens,
2003 block_ids,
2004 total_post_pad,
2005 gate_up_packed,
2006 batch,
2007 moe_block_size,
2008 top_k,
2009 )?;
2010 } else {
2011 gu_store.gemm_phase_vllm(
2012 ctx,
2013 x_packed,
2014 sorted_tokens,
2015 block_ids,
2016 total_post_pad,
2017 gate_up_packed,
2018 total_pairs_active,
2019 moe_block_size,
2020 1, )?;
2022 }
2023 } else {
2024 gu_store.gemm_phase_batched(
2025 ctx,
2026 x_packed,
2027 &phase1_dispatches,
2028 gate_up_packed,
2029 hidden_size,
2030 )?;
2031 }
2032 if let Some(t) = t_gemm1 {
2033 B::sync(ctx);
2034 MOE_BUCKET_GEMM1_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
2035 }
2036
2037 let total_pairs_active = batch * top_k;
2043 let t_silu = if prof {
2044 Some(std::time::Instant::now())
2045 } else {
2046 None
2047 };
2048 B::fused_silu_mul_split(
2049 ctx,
2050 gate_up_packed,
2051 silu_packed,
2052 total_pairs_active,
2053 expert_intermediate,
2054 );
2055 if let Some(t) = t_silu {
2056 B::sync(ctx);
2057 MOE_BUCKET_SILU_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
2058 }
2059
2060 let d_store = experts.down_stacked_store(0).ok_or_else(|| {
2062 FerrumError::model(
2063 "moe_forward_bucketed requires stacked down store \
2064 (load via Qwen3MoeModel::new_safetensors)",
2065 )
2066 })?;
2067 if zero_marlin_workspace {
2068 let _ = d_store.zero_workspace(ctx);
2069 }
2070 let phase3_dispatches: Vec<(usize, usize, usize, usize)> = if vllm_refs.is_none() {
2071 let plan = plan.expect("plan is Some when batched GEMM path runs");
2072 let mut v: Vec<(usize, usize, usize, usize)> = Vec::with_capacity(num_experts);
2073 for e in 0..num_experts {
2074 let m_e = plan.expert_offsets[e + 1] - plan.expert_offsets[e];
2075 if m_e == 0 {
2076 continue;
2077 }
2078 let pair_off = plan.expert_offsets[e];
2079 v.push((e, pair_off, pair_off, m_e));
2080 }
2081 v.sort_by(|a, b| b.3.cmp(&a.3).then_with(|| a.0.cmp(&b.0)));
2082 v
2083 } else {
2084 Vec::new()
2085 };
2086 let t_gemm3 = if prof {
2087 Some(std::time::Instant::now())
2088 } else {
2089 None
2090 };
2091 if let Some((sorted_tokens, block_ids, total_post_pad)) = vllm_refs {
2092 #[cfg(feature = "cuda")]
2093 let _marlin_label = if ferrum_kernels::backend::cuda::marlin::profile_marlin() {
2094 Some(ferrum_kernels::backend::cuda::push_alloc_label(
2095 "moe.vllm.down_proj",
2096 ))
2097 } else {
2098 None
2099 };
2100 d_store.gemm_phase_vllm(
2101 ctx,
2102 silu_packed,
2103 sorted_tokens,
2104 block_ids,
2105 total_post_pad,
2106 down_packed,
2107 total_pairs_active,
2108 moe_block_size,
2109 1,
2110 )?;
2111 } else {
2112 d_store.gemm_phase_batched(
2113 ctx,
2114 silu_packed,
2115 &phase3_dispatches,
2116 down_packed,
2117 expert_intermediate,
2118 )?;
2119 }
2120 if let Some(t) = t_gemm3 {
2121 B::sync(ctx);
2122 MOE_BUCKET_GEMM3_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
2123 }
2124
2125 let total_pairs = batch * top_k;
2142 let t_comb = if prof {
2143 Some(std::time::Instant::now())
2144 } else {
2145 None
2146 };
2147 if use_vllm_pair_ids {
2148 let dr = dr_kept
2149 .as_ref()
2150 .expect("dr_kept is Some when use_vllm_pair_ids");
2151 B::weighted_sum_batched(
2152 ctx,
2153 down_packed,
2154 dr.pair_weights,
2155 out,
2156 batch,
2157 top_k,
2158 hidden_size,
2159 )?;
2160 } else {
2161 let (pairs_ref, weights_ref);
2162 let _pairs_owned;
2163 let _weights_owned;
2164 if let Some(ref dr) = dr_kept {
2165 pairs_ref = &*dr.pairs_by_token;
2166 weights_ref = &*dr.pair_weights;
2167 } else {
2168 let plan = plan.expect("plan is Some when host moe_combine runs");
2169 _pairs_owned = B::from_slice_typed::<i32>(&plan.pairs_by_token);
2170 _weights_owned = B::from_slice_typed::<f32>(&plan.pair_weights);
2171 pairs_ref = &_pairs_owned;
2172 weights_ref = &_weights_owned;
2173 }
2174 B::moe_combine(
2175 ctx,
2176 down_packed,
2177 pairs_ref,
2178 weights_ref,
2179 out,
2180 batch,
2181 hidden_size,
2182 top_k,
2183 total_pairs,
2184 );
2185 }
2186 if let Some(t) = t_comb {
2187 B::sync(ctx);
2188 MOE_BUCKET_COMBINE_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
2189 }
2190
2191 Ok(())
2192}
2193
2194pub fn moe_forward_cpu(
2209 x: &[f32],
2210 batch: usize,
2211 hidden_size: usize,
2212 expert_intermediate: usize,
2213 top_k: usize,
2214 router: &RouterOutput,
2215 experts: &ExpertStack<CpuBackend>,
2216 out: &mut Vec<f32>,
2217) -> Result<()> {
2218 let n_experts = experts.num_experts();
2219
2220 if x.len() != batch * hidden_size {
2221 return Err(FerrumError::model(format!(
2222 "moe_forward_cpu: x len {} doesn't match batch*hidden = {}*{} = {}",
2223 x.len(),
2224 batch,
2225 hidden_size,
2226 batch * hidden_size
2227 )));
2228 }
2229 if router.expert_ids.len() != batch * top_k {
2230 return Err(FerrumError::model(format!(
2231 "moe_forward_cpu: router has {} expert_ids but expected batch*top_k = {}*{} = {}",
2232 router.expert_ids.len(),
2233 batch,
2234 top_k,
2235 batch * top_k
2236 )));
2237 }
2238
2239 out.clear();
2240 out.resize(batch * hidden_size, 0.0);
2241
2242 let mut ctx = <CpuBackend as Backend>::new_context();
2243 let mut x_b: Vec<f32> = vec![0.0; hidden_size];
2244 let mut gate_up_buf: Vec<f32> = vec![0.0; 2 * expert_intermediate];
2245 let mut silu_mul_buf: Vec<f32> = vec![0.0; expert_intermediate];
2246 let mut down_out: Vec<f32> = vec![0.0; hidden_size];
2247
2248 for b in 0..batch {
2249 x_b.copy_from_slice(&x[b * hidden_size..(b + 1) * hidden_size]);
2250
2251 for k in 0..top_k {
2252 let pair_idx = b * top_k + k;
2253 let expert_id = router.expert_ids[pair_idx] as usize;
2254 let weight = router.expert_weights[pair_idx];
2255
2256 if expert_id >= n_experts {
2257 return Err(FerrumError::model(format!(
2258 "moe_forward_cpu: router selected expert {expert_id} >= num_experts {n_experts}"
2259 )));
2260 }
2261
2262 experts.gate_up[expert_id].forward(&mut ctx, &x_b, &mut gate_up_buf, 1);
2264
2265 <CpuBackend as Backend>::fused_silu_mul_split(
2267 &mut ctx,
2268 &gate_up_buf,
2269 &mut silu_mul_buf,
2270 1,
2271 expert_intermediate,
2272 );
2273
2274 experts.down[expert_id].forward(&mut ctx, &silu_mul_buf, &mut down_out, 1);
2276
2277 let out_row = &mut out[b * hidden_size..(b + 1) * hidden_size];
2281 for (o, d) in out_row.iter_mut().zip(down_out.iter()) {
2282 *o += weight * *d;
2283 }
2284 }
2285 }
2286
2287 Ok(())
2288}
2289
2290fn check_size(actual: usize, expected: usize, label: &str) -> Result<()> {
2291 if actual != expected {
2292 return Err(FerrumError::model(format!(
2293 "ExpertStack: {label} size mismatch (got {actual}, expected {expected})"
2294 )));
2295 }
2296 Ok(())
2297}
2298
2299fn quant_kind(gguf: &GgufFile, name: &str) -> Result<Option<GgufQuantType>> {
2303 let info = gguf.tensor_info(name).ok_or_else(|| {
2304 FerrumError::model(format!("ExpertStack: tensor info missing for '{name}'"))
2305 })?;
2306 Ok(match info.ggml_dtype {
2307 GgmlDType::Q4K => Some(GgufQuantType::Q4K),
2308 GgmlDType::Q6K => Some(GgufQuantType::Q6K),
2309 _ => None,
2310 })
2311}
2312
2313fn block_bytes_for(kind: GgufQuantType, n_elems: usize, label: &str) -> Result<usize> {
2318 const QK_K: usize = 256;
2319 if n_elems % QK_K != 0 {
2320 return Err(FerrumError::model(format!(
2321 "ExpertStack {label}: per-expert element count {n_elems} not a multiple of {QK_K}"
2322 )));
2323 }
2324 let block_bytes = match kind {
2325 GgufQuantType::Q4K => 144,
2326 GgufQuantType::Q6K => 210,
2327 other => {
2330 return Err(FerrumError::model(format!(
2331 "ExpertStack {label}: unsupported k-quant flavour {other:?}"
2332 )))
2333 }
2334 };
2335 Ok((n_elems / QK_K) * block_bytes)
2336}
2337
2338fn read_dequant_flat(gguf: &GgufFile, name: &str, device: &Device) -> Result<Vec<f32>> {
2339 let qt = gguf.read_tensor(name, device).map_err(candle_to_ferrum)?;
2340 let dense = qt.dequantize(device).map_err(candle_to_ferrum)?;
2341 let flat = dense.flatten_all().map_err(candle_to_ferrum)?;
2342 flat.to_vec1::<f32>().map_err(candle_to_ferrum)
2343}
2344
2345fn candle_to_ferrum(e: candle_core::Error) -> FerrumError {
2346 FerrumError::model(format!("candle: {e}"))
2347}
2348
2349#[allow(dead_code)]
2352type _CandleResult<T> = CandleResult<T>;
2353
2354#[cfg(test)]
2355mod tests {
2356 use std::sync::atomic::Ordering;
2357
2358 use ferrum_kernels::backend::cpu::CpuBackend;
2359 use ferrum_kernels::backend::Backend;
2360 use ferrum_kernels::StackedExpertGgufLinear;
2361
2362 use super::{
2363 drain_moe_bucket_profile, pick_moe_block_size_with_config, ExpertStack,
2364 MoeDispatchRuntimeConfig, MOE_BUCKET_COMBINE_US, MOE_BUCKET_D2H_US, MOE_BUCKET_GATHER_US,
2365 MOE_BUCKET_GEMM1_US, MOE_BUCKET_GEMM3_US, MOE_BUCKET_LAYER_CALLS, MOE_BUCKET_PLAN_US,
2366 MOE_BUCKET_ROUTE_US, MOE_BUCKET_SILU_US, MOE_BUCKET_SYNC_US,
2367 };
2368
2369 struct FakeStackedGgufLinear {
2370 num_experts: usize,
2371 rows: usize,
2372 cols: usize,
2373 }
2374
2375 impl StackedExpertGgufLinear<CpuBackend> for FakeStackedGgufLinear {
2376 fn num_experts(&self) -> usize {
2377 self.num_experts
2378 }
2379
2380 fn n_rows(&self) -> usize {
2381 self.rows
2382 }
2383
2384 fn n_cols(&self) -> usize {
2385 self.cols
2386 }
2387
2388 fn as_any(&self) -> &dyn std::any::Any {
2389 self
2390 }
2391
2392 fn gemv_moe_id(
2393 &self,
2394 _ctx: &mut <CpuBackend as Backend>::Context,
2395 _a: &<CpuBackend as Backend>::Buffer,
2396 _ids: &<CpuBackend as Backend>::Buffer,
2397 _out: &mut <CpuBackend as Backend>::Buffer,
2398 _n_selected: usize,
2399 _src1_stride: usize,
2400 ) -> ferrum_types::Result<()> {
2401 unimplemented!("num_experts test does not dispatch kernels")
2402 }
2403
2404 fn gemv_moe_id_offset(
2405 &self,
2406 _ctx: &mut <CpuBackend as Backend>::Context,
2407 _a: &<CpuBackend as Backend>::Buffer,
2408 _a_offset: usize,
2409 _ids: &<CpuBackend as Backend>::Buffer,
2410 _ids_offset: usize,
2411 _out: &mut <CpuBackend as Backend>::Buffer,
2412 _n_selected: usize,
2413 _src1_stride: usize,
2414 ) -> ferrum_types::Result<()> {
2415 unimplemented!("num_experts test does not dispatch kernels")
2416 }
2417
2418 fn gemv_moe_id_gate_up_silu(
2419 &self,
2420 _ctx: &mut <CpuBackend as Backend>::Context,
2421 _a: &<CpuBackend as Backend>::Buffer,
2422 _other_up: &dyn StackedExpertGgufLinear<CpuBackend>,
2423 _ids: &<CpuBackend as Backend>::Buffer,
2424 _silu_out: &mut <CpuBackend as Backend>::Buffer,
2425 _n_selected: usize,
2426 ) -> ferrum_types::Result<()> {
2427 unimplemented!("num_experts test does not dispatch kernels")
2428 }
2429
2430 fn gemv_moe_id_batched(
2431 &self,
2432 _ctx: &mut <CpuBackend as Backend>::Context,
2433 _a: &<CpuBackend as Backend>::Buffer,
2434 _ids: &<CpuBackend as Backend>::Buffer,
2435 _out: &mut <CpuBackend as Backend>::Buffer,
2436 _m: usize,
2437 _top_k: usize,
2438 _src1_outer_stride: usize,
2439 _src1_inner_stride: usize,
2440 ) -> ferrum_types::Result<()> {
2441 unimplemented!("num_experts test does not dispatch kernels")
2442 }
2443
2444 fn gemv_moe_id_gate_up_silu_batched(
2445 &self,
2446 _ctx: &mut <CpuBackend as Backend>::Context,
2447 _a: &<CpuBackend as Backend>::Buffer,
2448 _other_up: &dyn StackedExpertGgufLinear<CpuBackend>,
2449 _ids: &<CpuBackend as Backend>::Buffer,
2450 _silu_out: &mut <CpuBackend as Backend>::Buffer,
2451 _m: usize,
2452 _top_k: usize,
2453 _src1_outer_stride: usize,
2454 _src1_inner_stride: usize,
2455 ) -> ferrum_types::Result<()> {
2456 unimplemented!("num_experts test does not dispatch kernels")
2457 }
2458
2459 fn gemm_moe_id(
2460 &self,
2461 _ctx: &mut <CpuBackend as Backend>::Context,
2462 _a: &<CpuBackend as Backend>::Buffer,
2463 _ids: &<CpuBackend as Backend>::Buffer,
2464 _tpe: &<CpuBackend as Backend>::Buffer,
2465 _out: &mut <CpuBackend as Backend>::Buffer,
2466 _ne11: usize,
2467 _top_k: usize,
2468 _max_per_expert: usize,
2469 _batch: usize,
2470 ) -> ferrum_types::Result<()> {
2471 unimplemented!("num_experts test does not dispatch kernels")
2472 }
2473
2474 fn gemm_moe_id_indirect(
2475 &self,
2476 _ctx: &mut <CpuBackend as Backend>::Context,
2477 _src1: &<CpuBackend as Backend>::Buffer,
2478 _ids: &<CpuBackend as Backend>::Buffer,
2479 _tpe: &<CpuBackend as Backend>::Buffer,
2480 _out: &mut <CpuBackend as Backend>::Buffer,
2481 _args_buf: &<CpuBackend as Backend>::Buffer,
2482 _ne11: usize,
2483 _top_k: usize,
2484 _max_per_expert: usize,
2485 _batch: usize,
2486 ) -> ferrum_types::Result<()> {
2487 unimplemented!("num_experts test does not dispatch kernels")
2488 }
2489 }
2490
2491 fn fake_stacked(num_experts: usize) -> Box<dyn StackedExpertGgufLinear<CpuBackend>> {
2492 Box::new(FakeStackedGgufLinear {
2493 num_experts,
2494 rows: 4,
2495 cols: 4,
2496 })
2497 }
2498
2499 #[test]
2500 fn expert_stack_num_experts_uses_stacked_fast_path_count() {
2501 let experts = ExpertStack::<CpuBackend> {
2502 gate_up: Vec::new(),
2503 down: Vec::new(),
2504 gate_stacked: Some(fake_stacked(7)),
2505 up_stacked: Some(fake_stacked(7)),
2506 down_stacked: Some(fake_stacked(7)),
2507 gate_up_marlin_stack: None,
2508 down_marlin_stack: None,
2509 };
2510
2511 assert_eq!(experts.num_experts(), 7);
2512 }
2513
2514 #[test]
2515 fn moe_dispatch_runtime_config_parses_m3_startup_knobs() {
2516 let config = MoeDispatchRuntimeConfig::from_env_vars([
2517 ("FERRUM_MOE_PROFILE", "0"),
2518 ("FERRUM_DECODE_OP_PROFILE", "true"),
2519 ("FERRUM_VLLM_MOE_ZERO_WS", "1"),
2520 ("FERRUM_VLLM_MOE_PAIR_IDS", "1"),
2521 ("FERRUM_MOE_LOAD_TRACE", ""),
2522 ("FERRUM_MOE_BLOCK_SIZE", "8"),
2523 ("FERRUM_MOE_LARGE_M_BLOCK_SIZE", "64"),
2524 ("FERRUM_MOE_LARGE_M_MIN_PAIRS", "2048"),
2525 ("FERRUM_VLLM_MOE", "1"),
2526 ("FERRUM_MOE_HOST_ROUTE", "1"),
2527 ]);
2528
2529 assert!(config.moe_profile);
2530 assert!(config.decode_op_profile);
2531 assert!(config.vllm_moe_zero_ws);
2532 assert!(config.vllm_moe_pair_ids);
2533 assert!(config.moe_load_trace);
2534 assert_eq!(config.moe_block_size, Some(8));
2535 assert_eq!(config.moe_large_m_block_size, Some(64));
2536 assert_eq!(config.moe_large_m_min_pairs, 2048);
2537 assert!(config.vllm_moe);
2538 assert!(config.moe_host_route);
2539 }
2540
2541 #[test]
2542 fn drain_moe_bucket_profile_returns_and_clears_counters() {
2543 let _ = drain_moe_bucket_profile();
2544 MOE_BUCKET_LAYER_CALLS.store(2, Ordering::Relaxed);
2545 MOE_BUCKET_SYNC_US.store(3, Ordering::Relaxed);
2546 MOE_BUCKET_D2H_US.store(5, Ordering::Relaxed);
2547 MOE_BUCKET_ROUTE_US.store(7, Ordering::Relaxed);
2548 MOE_BUCKET_PLAN_US.store(11, Ordering::Relaxed);
2549 MOE_BUCKET_GATHER_US.store(13, Ordering::Relaxed);
2550 MOE_BUCKET_GEMM1_US.store(17, Ordering::Relaxed);
2551 MOE_BUCKET_SILU_US.store(19, Ordering::Relaxed);
2552 MOE_BUCKET_GEMM3_US.store(23, Ordering::Relaxed);
2553 MOE_BUCKET_COMBINE_US.store(29, Ordering::Relaxed);
2554
2555 let snapshot = drain_moe_bucket_profile();
2556 assert_eq!(snapshot.layers, 2);
2557 assert_eq!(snapshot.total_us(), 127);
2558 assert!(snapshot.has_layers());
2559
2560 let cleared = drain_moe_bucket_profile();
2561 assert_eq!(cleared.layers, 0);
2562 assert_eq!(cleared.total_us(), 0);
2563 assert!(!cleared.has_layers());
2564 }
2565
2566 #[test]
2567 fn moe_dispatch_runtime_config_bounds_invalid_block_values() {
2568 let config = MoeDispatchRuntimeConfig::from_env_vars([
2569 ("FERRUM_MOE_BLOCK_SIZE", "12"),
2570 ("FERRUM_MOE_LARGE_M_BLOCK_SIZE", "128"),
2571 ("FERRUM_MOE_LARGE_M_MIN_PAIRS", "bad"),
2572 ("FERRUM_VLLM_MOE_ZERO_WS", "true"),
2573 ("FERRUM_MOE_HOST_ROUTE", "0"),
2574 ]);
2575
2576 assert_eq!(config.moe_block_size, None);
2577 assert_eq!(config.moe_large_m_block_size, None);
2578 assert_eq!(config.moe_large_m_min_pairs, 1024);
2579 assert!(!config.vllm_moe_zero_ws);
2580 assert!(!config.moe_host_route);
2581 }
2582
2583 #[test]
2584 fn device_route_large_m_block_size_is_thresholded() {
2585 let config = MoeDispatchRuntimeConfig::from_env_vars([
2586 ("FERRUM_MOE_LARGE_M_BLOCK_SIZE", "64"),
2587 ("FERRUM_MOE_LARGE_M_MIN_PAIRS", "1024"),
2588 ]);
2589
2590 assert_eq!(
2591 pick_moe_block_size_with_config(&config, None, 128, true, 256),
2592 16
2593 );
2594 assert_eq!(
2595 pick_moe_block_size_with_config(&config, None, 128, true, 1024),
2596 64
2597 );
2598 }
2599
2600 #[test]
2601 fn global_moe_block_size_override_still_wins() {
2602 let config = MoeDispatchRuntimeConfig::from_env_vars([
2603 ("FERRUM_MOE_BLOCK_SIZE", "32"),
2604 ("FERRUM_MOE_LARGE_M_BLOCK_SIZE", "64"),
2605 ("FERRUM_MOE_LARGE_M_MIN_PAIRS", "1024"),
2606 ]);
2607
2608 assert_eq!(
2609 pick_moe_block_size_with_config(&config, None, 128, true, 2048),
2610 32
2611 );
2612 }
2613}