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 {
1471 pub output: RouterOutput,
1472 pub probs: Vec<f32>,
1475 pub plan: MoeBucketPlan,
1476}
1477
1478impl MoeRouteScratch {
1479 pub fn new() -> Self {
1480 Self {
1481 output: RouterOutput::empty(),
1482 probs: Vec::new(),
1483 plan: MoeBucketPlan::empty(),
1484 }
1485 }
1486}
1487
1488impl Default for MoeRouteScratch {
1489 fn default() -> Self {
1490 Self::new()
1491 }
1492}
1493
1494pub struct DeviceRouteScratch<'a, B: crate::moe::dispatch::Backend> {
1503 pub selected_ids: &'a mut B::Buffer,
1504 pub pair_weights: &'a mut B::Buffer,
1505 pub pairs_by_token: &'a mut B::Buffer,
1506 pub packed_token_idx: &'a mut B::Buffer,
1507 pub expert_offsets: &'a mut B::Buffer,
1508 pub sorted_tokens: &'a mut B::Buffer,
1512 pub block_ids: &'a mut B::Buffer,
1513 pub total_post_pad: &'a mut B::Buffer,
1514}
1515
1516pub struct MoeForwardBucketedParams<'a, B: QuantLlmBackend + BackendMoeFused> {
1533 pub ctx: &'a mut B::Context,
1534 pub x: &'a B::Buffer,
1535 pub router_logits: &'a B::Buffer,
1536 pub out: &'a mut B::Buffer,
1537 pub batch: usize,
1538 pub hidden_size: usize,
1539 pub expert_intermediate: usize,
1540 pub num_experts: usize,
1541 pub top_k: usize,
1542 pub norm_topk_prob: bool,
1543 pub experts: &'a ExpertStack<B>,
1544 pub x_packed: &'a mut B::Buffer,
1545 pub gate_up_packed: &'a mut B::Buffer,
1546 pub silu_packed: &'a mut B::Buffer,
1547 pub down_packed: &'a mut B::Buffer,
1548 pub route_scratch: &'a mut MoeRouteScratch,
1549 pub profile_bucket: bool,
1550 pub device_route: Option<DeviceRouteScratch<'a, B>>,
1555}
1556
1557pub fn moe_forward_bucketed<B: QuantLlmBackend + BackendMoeFused>(
1558 params: MoeForwardBucketedParams<'_, B>,
1559) -> Result<()> {
1560 let MoeForwardBucketedParams {
1561 ctx,
1562 x,
1563 router_logits,
1564 out,
1565 batch,
1566 hidden_size,
1567 expert_intermediate,
1568 num_experts,
1569 top_k,
1570 norm_topk_prob,
1571 experts,
1572 x_packed,
1573 gate_up_packed,
1574 silu_packed,
1575 down_packed,
1576 route_scratch,
1577 profile_bucket,
1578 device_route,
1579 } = params;
1580 if experts.num_experts() != num_experts {
1581 return Err(FerrumError::model(format!(
1582 "moe_forward_bucketed: experts {} != num_experts {num_experts}",
1583 experts.num_experts()
1584 )));
1585 }
1586
1587 let runtime_config = moe_dispatch_runtime_config();
1588 let prof = profile_bucket || runtime_config.moe_profile || runtime_config.decode_op_profile;
1591 if prof {
1592 MOE_BUCKET_LAYER_CALLS.fetch_add(1, Ordering::Relaxed);
1593 }
1594
1595 let stack_requires_vllm = experts
1609 .gate_up_marlin_stack
1610 .as_ref()
1611 .is_some_and(|stack| stack.requires_vllm_moe())
1612 || experts
1613 .down_marlin_stack
1614 .as_ref()
1615 .is_some_and(|stack| stack.requires_vllm_moe());
1616 let use_vllm_moe = runtime_config.vllm_moe || stack_requires_vllm;
1617 let use_device_route = device_route.is_some() && use_vllm_moe && !runtime_config.moe_host_route;
1631 let use_vllm_pair_ids = use_device_route && runtime_config.vllm_moe_pair_ids;
1632
1633 let mut dr_kept: Option<DeviceRouteScratch<'_, B>> = if use_device_route {
1640 let dr = device_route.expect("device_route is Some when use_device_route");
1641 B::route_topk_softmax(
1642 ctx,
1643 router_logits,
1644 dr.selected_ids,
1645 dr.pair_weights,
1646 batch,
1647 num_experts,
1648 top_k,
1649 norm_topk_prob,
1650 )?;
1651 if !use_vllm_pair_ids {
1652 B::moe_build_pairs_by_token(
1653 ctx,
1654 dr.selected_ids,
1655 dr.pairs_by_token,
1656 dr.packed_token_idx,
1657 dr.expert_offsets,
1658 batch * top_k,
1659 num_experts,
1660 top_k,
1661 )?;
1662 }
1663 Some(dr)
1664 } else {
1665 None
1666 };
1667
1668 let plan: Option<&crate::moe::MoeBucketPlan> = if !use_device_route {
1680 let t_route_total = if prof {
1681 Some(std::time::Instant::now())
1682 } else {
1683 None
1684 };
1685 let gpu_route = B::try_gpu_route_topk_into_host(
1686 ctx,
1687 router_logits,
1688 &mut route_scratch.output.expert_ids,
1689 &mut route_scratch.output.expert_weights,
1690 batch,
1691 num_experts,
1692 top_k,
1693 norm_topk_prob,
1694 );
1695 if gpu_route.is_err() {
1696 let t_sync = if prof {
1697 Some(std::time::Instant::now())
1698 } else {
1699 None
1700 };
1701 B::sync(ctx);
1702 if let Some(t) = t_sync {
1703 MOE_BUCKET_SYNC_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
1704 }
1705 let t_d2h = if prof {
1706 Some(std::time::Instant::now())
1707 } else {
1708 None
1709 };
1710 let logits_host = B::to_vec(router_logits, batch * num_experts);
1711 if let Some(t) = t_d2h {
1712 MOE_BUCKET_D2H_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
1713 }
1714 let t_route = if prof {
1715 Some(std::time::Instant::now())
1716 } else {
1717 None
1718 };
1719 crate::moe::router::route_into(
1720 &logits_host,
1721 batch,
1722 num_experts,
1723 top_k,
1724 norm_topk_prob,
1725 &mut route_scratch.output,
1726 &mut route_scratch.probs,
1727 );
1728 if let Some(t) = t_route {
1729 MOE_BUCKET_ROUTE_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
1730 }
1731 } else if let Some(t) = t_route_total {
1732 MOE_BUCKET_ROUTE_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
1733 }
1734 let t_plan = if prof {
1735 Some(std::time::Instant::now())
1736 } else {
1737 None
1738 };
1739 route_scratch
1740 .plan
1741 .rebuild_into(&route_scratch.output, batch, num_experts, top_k);
1742 if let Some(t) = t_plan {
1743 MOE_BUCKET_PLAN_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
1744 }
1745 Some(&route_scratch.plan)
1746 } else {
1747 None
1748 };
1749
1750 if !use_vllm_pair_ids {
1754 let t_gather = if prof {
1755 Some(std::time::Instant::now())
1756 } else {
1757 None
1758 };
1759 if let Some(ref dr) = dr_kept {
1760 B::embedding_lookup_dev(
1761 ctx,
1762 x,
1763 dr.packed_token_idx,
1764 x_packed,
1765 batch * top_k,
1766 hidden_size,
1767 );
1768 } else {
1769 let plan = plan.expect("plan is Some when !use_device_route");
1770 B::embedding_lookup(ctx, x, &plan.packed_token_idx, x_packed, hidden_size);
1771 }
1772 if let Some(t) = t_gather {
1773 B::sync(ctx);
1774 MOE_BUCKET_GATHER_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
1775 }
1776 }
1777
1778 let gate_up_dim_per_expert = 2 * expert_intermediate;
1786 let down_n_per_expert = hidden_size;
1787 let gu_store = experts.gate_up_stacked_store(0).ok_or_else(|| {
1793 FerrumError::model(
1794 "moe_forward_bucketed requires stacked gate_up store \
1795 (load via Qwen3MoeModel::new_safetensors)",
1796 )
1797 })?;
1798 let zero_marlin_workspace = !use_vllm_moe || runtime_config.vllm_moe_zero_ws;
1799 if zero_marlin_workspace {
1800 let _ = gu_store.zero_workspace(ctx);
1801 }
1802
1803 let total_pairs_active = batch * top_k;
1809 let max_block_size: usize = 64;
1836 let moe_block_size: usize = pick_moe_block_size_with_config(
1837 runtime_config,
1838 plan,
1839 num_experts,
1840 use_device_route,
1841 total_pairs_active,
1842 );
1843 debug_assert!(
1844 moe_block_size <= max_block_size,
1845 "moe_block_size {moe_block_size} exceeds scratch worst-case {max_block_size}"
1846 );
1847 let sorted_max_size = batch * top_k + num_experts * moe_block_size;
1855 let vllm_routing_owned: Option<ferrum_kernels::backend::MoeRouting<B>> =
1856 if use_vllm_moe && !use_device_route {
1857 let plan = plan.expect("plan is Some when host vllm builder runs");
1858 let mut padded_offsets = Vec::with_capacity(num_experts + 1);
1859 let mut acc = 0usize;
1860 for e in 0..num_experts {
1861 padded_offsets.push(acc);
1862 let m_e = plan.expert_offsets[e + 1] - plan.expert_offsets[e];
1863 let pe = m_e.div_ceil(moe_block_size) * moe_block_size;
1864 acc += pe;
1865 }
1866 padded_offsets.push(acc);
1867 let total_padded = acc;
1868 let total_blocks = total_padded / moe_block_size;
1869 let sentinel = total_pairs_active as i32;
1870
1871 let mut sorted_token_ids = vec![sentinel; total_padded];
1872 let mut expert_ids = vec![0i32; total_blocks];
1873 for e in 0..num_experts {
1874 let m_e = plan.expert_offsets[e + 1] - plan.expert_offsets[e];
1875 if m_e == 0 {
1876 continue;
1877 }
1878 let p_off = padded_offsets[e];
1879 let real_off = plan.expert_offsets[e];
1880 for i in 0..m_e {
1881 sorted_token_ids[p_off + i] = (real_off + i) as i32;
1882 }
1883 let blocks_for_e = (padded_offsets[e + 1] - p_off) / moe_block_size;
1884 let block_start = p_off / moe_block_size;
1885 for b in 0..blocks_for_e {
1886 expert_ids[block_start + b] = e as i32;
1887 }
1888 }
1889 let num_tokens_past_padded = vec![total_padded as i32];
1890 Some(B::upload_moe_routing(
1891 ctx,
1892 &sorted_token_ids,
1893 &expert_ids,
1894 &num_tokens_past_padded,
1895 )?)
1896 } else {
1897 None
1898 };
1899
1900 if use_device_route {
1904 let dr = dr_kept
1905 .as_mut()
1906 .expect("dr_kept is Some when use_device_route");
1907 if use_vllm_pair_ids {
1908 B::moe_align_block_size_pair_ids(
1909 ctx,
1910 dr.selected_ids,
1911 dr.sorted_tokens,
1912 dr.block_ids,
1913 dr.total_post_pad,
1914 batch * top_k,
1915 num_experts,
1916 moe_block_size,
1917 sorted_max_size,
1918 )?;
1919 } else {
1920 B::moe_align_block_size(
1921 ctx,
1922 dr.selected_ids,
1923 dr.sorted_tokens,
1924 dr.block_ids,
1925 dr.total_post_pad,
1926 batch * top_k,
1927 num_experts,
1928 moe_block_size,
1929 sorted_max_size,
1930 )?;
1931 }
1932 }
1933
1934 let vllm_refs: Option<(&B::Buffer, &B::Buffer, &B::Buffer)> = if use_device_route {
1939 let dr = dr_kept
1940 .as_ref()
1941 .expect("dr_kept is Some when use_device_route");
1942 Some((&*dr.sorted_tokens, &*dr.block_ids, &*dr.total_post_pad))
1943 } else if let Some(r) = vllm_routing_owned.as_ref() {
1944 Some((
1945 &r.sorted_token_ids,
1946 &r.expert_ids,
1947 &r.num_tokens_past_padded,
1948 ))
1949 } else {
1950 None
1951 };
1952
1953 let phase1_dispatches: Vec<(usize, usize, usize, usize)> = if vllm_refs.is_none() {
1957 let plan = plan.expect("plan is Some when batched GEMM path runs");
1958 let mut v: Vec<(usize, usize, usize, usize)> = Vec::with_capacity(num_experts);
1959 for e in 0..num_experts {
1960 let m_e = plan.expert_offsets[e + 1] - plan.expert_offsets[e];
1961 if m_e == 0 {
1962 continue;
1963 }
1964 let pair_off = plan.expert_offsets[e];
1965 v.push((e, pair_off, pair_off, m_e));
1966 }
1967 v.sort_by(|a, b| b.3.cmp(&a.3).then_with(|| a.0.cmp(&b.0)));
1968 v
1969 } else {
1970 Vec::new()
1971 };
1972 let t_gemm1 = if prof {
1973 Some(std::time::Instant::now())
1974 } else {
1975 None
1976 };
1977 if let Some((sorted_tokens, block_ids, total_post_pad)) = vllm_refs {
1978 #[cfg(feature = "cuda")]
1980 let _marlin_label = if ferrum_kernels::backend::cuda::marlin::profile_marlin() {
1981 Some(ferrum_kernels::backend::cuda::push_alloc_label(
1982 "moe.vllm.gate_up_proj",
1983 ))
1984 } else {
1985 None
1986 };
1987 if use_vllm_pair_ids {
1988 gu_store.gemm_phase_vllm(
1989 ctx,
1990 x,
1991 sorted_tokens,
1992 block_ids,
1993 total_post_pad,
1994 gate_up_packed,
1995 batch,
1996 moe_block_size,
1997 top_k,
1998 )?;
1999 } else {
2000 gu_store.gemm_phase_vllm(
2001 ctx,
2002 x_packed,
2003 sorted_tokens,
2004 block_ids,
2005 total_post_pad,
2006 gate_up_packed,
2007 total_pairs_active,
2008 moe_block_size,
2009 1, )?;
2011 }
2012 } else {
2013 gu_store.gemm_phase_batched(
2014 ctx,
2015 x_packed,
2016 &phase1_dispatches,
2017 gate_up_packed,
2018 hidden_size,
2019 )?;
2020 }
2021 if let Some(t) = t_gemm1 {
2022 B::sync(ctx);
2023 MOE_BUCKET_GEMM1_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
2024 }
2025
2026 let total_pairs_active = batch * top_k;
2032 let t_silu = if prof {
2033 Some(std::time::Instant::now())
2034 } else {
2035 None
2036 };
2037 B::fused_silu_mul_split(
2038 ctx,
2039 gate_up_packed,
2040 silu_packed,
2041 total_pairs_active,
2042 expert_intermediate,
2043 );
2044 if let Some(t) = t_silu {
2045 B::sync(ctx);
2046 MOE_BUCKET_SILU_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
2047 }
2048
2049 let d_store = experts.down_stacked_store(0).ok_or_else(|| {
2051 FerrumError::model(
2052 "moe_forward_bucketed requires stacked down store \
2053 (load via Qwen3MoeModel::new_safetensors)",
2054 )
2055 })?;
2056 if zero_marlin_workspace {
2057 let _ = d_store.zero_workspace(ctx);
2058 }
2059 let phase3_dispatches: Vec<(usize, usize, usize, usize)> = if vllm_refs.is_none() {
2060 let plan = plan.expect("plan is Some when batched GEMM path runs");
2061 let mut v: Vec<(usize, usize, usize, usize)> = Vec::with_capacity(num_experts);
2062 for e in 0..num_experts {
2063 let m_e = plan.expert_offsets[e + 1] - plan.expert_offsets[e];
2064 if m_e == 0 {
2065 continue;
2066 }
2067 let pair_off = plan.expert_offsets[e];
2068 v.push((e, pair_off, pair_off, m_e));
2069 }
2070 v.sort_by(|a, b| b.3.cmp(&a.3).then_with(|| a.0.cmp(&b.0)));
2071 v
2072 } else {
2073 Vec::new()
2074 };
2075 let t_gemm3 = if prof {
2076 Some(std::time::Instant::now())
2077 } else {
2078 None
2079 };
2080 if let Some((sorted_tokens, block_ids, total_post_pad)) = vllm_refs {
2081 #[cfg(feature = "cuda")]
2082 let _marlin_label = if ferrum_kernels::backend::cuda::marlin::profile_marlin() {
2083 Some(ferrum_kernels::backend::cuda::push_alloc_label(
2084 "moe.vllm.down_proj",
2085 ))
2086 } else {
2087 None
2088 };
2089 d_store.gemm_phase_vllm(
2090 ctx,
2091 silu_packed,
2092 sorted_tokens,
2093 block_ids,
2094 total_post_pad,
2095 down_packed,
2096 total_pairs_active,
2097 moe_block_size,
2098 1,
2099 )?;
2100 } else {
2101 d_store.gemm_phase_batched(
2102 ctx,
2103 silu_packed,
2104 &phase3_dispatches,
2105 down_packed,
2106 expert_intermediate,
2107 )?;
2108 }
2109 if let Some(t) = t_gemm3 {
2110 B::sync(ctx);
2111 MOE_BUCKET_GEMM3_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
2112 }
2113
2114 let total_pairs = batch * top_k;
2131 let t_comb = if prof {
2132 Some(std::time::Instant::now())
2133 } else {
2134 None
2135 };
2136 if use_vllm_pair_ids {
2137 let dr = dr_kept
2138 .as_ref()
2139 .expect("dr_kept is Some when use_vllm_pair_ids");
2140 B::weighted_sum_batched(
2141 ctx,
2142 down_packed,
2143 dr.pair_weights,
2144 out,
2145 batch,
2146 top_k,
2147 hidden_size,
2148 )?;
2149 } else {
2150 let (pairs_ref, weights_ref);
2151 let _pairs_owned;
2152 let _weights_owned;
2153 if let Some(ref dr) = dr_kept {
2154 pairs_ref = &*dr.pairs_by_token;
2155 weights_ref = &*dr.pair_weights;
2156 } else {
2157 let plan = plan.expect("plan is Some when host moe_combine runs");
2158 _pairs_owned = B::from_slice_typed::<i32>(&plan.pairs_by_token);
2159 _weights_owned = B::from_slice_typed::<f32>(&plan.pair_weights);
2160 pairs_ref = &_pairs_owned;
2161 weights_ref = &_weights_owned;
2162 }
2163 B::moe_combine(
2164 ctx,
2165 down_packed,
2166 pairs_ref,
2167 weights_ref,
2168 out,
2169 batch,
2170 hidden_size,
2171 top_k,
2172 total_pairs,
2173 );
2174 }
2175 if let Some(t) = t_comb {
2176 B::sync(ctx);
2177 MOE_BUCKET_COMBINE_US.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
2178 }
2179
2180 Ok(())
2181}
2182
2183pub fn moe_forward_cpu(
2198 x: &[f32],
2199 batch: usize,
2200 hidden_size: usize,
2201 expert_intermediate: usize,
2202 top_k: usize,
2203 router: &RouterOutput,
2204 experts: &ExpertStack<CpuBackend>,
2205 out: &mut Vec<f32>,
2206) -> Result<()> {
2207 let n_experts = experts.num_experts();
2208
2209 if x.len() != batch * hidden_size {
2210 return Err(FerrumError::model(format!(
2211 "moe_forward_cpu: x len {} doesn't match batch*hidden = {}*{} = {}",
2212 x.len(),
2213 batch,
2214 hidden_size,
2215 batch * hidden_size
2216 )));
2217 }
2218 if router.expert_ids.len() != batch * top_k {
2219 return Err(FerrumError::model(format!(
2220 "moe_forward_cpu: router has {} expert_ids but expected batch*top_k = {}*{} = {}",
2221 router.expert_ids.len(),
2222 batch,
2223 top_k,
2224 batch * top_k
2225 )));
2226 }
2227
2228 out.clear();
2229 out.resize(batch * hidden_size, 0.0);
2230
2231 let mut ctx = <CpuBackend as Backend>::new_context();
2232 let mut x_b: Vec<f32> = vec![0.0; hidden_size];
2233 let mut gate_up_buf: Vec<f32> = vec![0.0; 2 * expert_intermediate];
2234 let mut silu_mul_buf: Vec<f32> = vec![0.0; expert_intermediate];
2235 let mut down_out: Vec<f32> = vec![0.0; hidden_size];
2236
2237 for b in 0..batch {
2238 x_b.copy_from_slice(&x[b * hidden_size..(b + 1) * hidden_size]);
2239
2240 for k in 0..top_k {
2241 let pair_idx = b * top_k + k;
2242 let expert_id = router.expert_ids[pair_idx] as usize;
2243 let weight = router.expert_weights[pair_idx];
2244
2245 if expert_id >= n_experts {
2246 return Err(FerrumError::model(format!(
2247 "moe_forward_cpu: router selected expert {expert_id} >= num_experts {n_experts}"
2248 )));
2249 }
2250
2251 experts.gate_up[expert_id].forward(&mut ctx, &x_b, &mut gate_up_buf, 1);
2253
2254 <CpuBackend as Backend>::fused_silu_mul_split(
2256 &mut ctx,
2257 &gate_up_buf,
2258 &mut silu_mul_buf,
2259 1,
2260 expert_intermediate,
2261 );
2262
2263 experts.down[expert_id].forward(&mut ctx, &silu_mul_buf, &mut down_out, 1);
2265
2266 let out_row = &mut out[b * hidden_size..(b + 1) * hidden_size];
2270 for (o, d) in out_row.iter_mut().zip(down_out.iter()) {
2271 *o += weight * *d;
2272 }
2273 }
2274 }
2275
2276 Ok(())
2277}
2278
2279fn check_size(actual: usize, expected: usize, label: &str) -> Result<()> {
2280 if actual != expected {
2281 return Err(FerrumError::model(format!(
2282 "ExpertStack: {label} size mismatch (got {actual}, expected {expected})"
2283 )));
2284 }
2285 Ok(())
2286}
2287
2288fn quant_kind(gguf: &GgufFile, name: &str) -> Result<Option<GgufQuantType>> {
2292 let info = gguf.tensor_info(name).ok_or_else(|| {
2293 FerrumError::model(format!("ExpertStack: tensor info missing for '{name}'"))
2294 })?;
2295 Ok(match info.ggml_dtype {
2296 GgmlDType::Q4K => Some(GgufQuantType::Q4K),
2297 GgmlDType::Q6K => Some(GgufQuantType::Q6K),
2298 _ => None,
2299 })
2300}
2301
2302fn block_bytes_for(kind: GgufQuantType, n_elems: usize, label: &str) -> Result<usize> {
2307 const QK_K: usize = 256;
2308 if n_elems % QK_K != 0 {
2309 return Err(FerrumError::model(format!(
2310 "ExpertStack {label}: per-expert element count {n_elems} not a multiple of {QK_K}"
2311 )));
2312 }
2313 let block_bytes = match kind {
2314 GgufQuantType::Q4K => 144,
2315 GgufQuantType::Q6K => 210,
2316 other => {
2319 return Err(FerrumError::model(format!(
2320 "ExpertStack {label}: unsupported k-quant flavour {other:?}"
2321 )))
2322 }
2323 };
2324 Ok((n_elems / QK_K) * block_bytes)
2325}
2326
2327fn read_dequant_flat(gguf: &GgufFile, name: &str, device: &Device) -> Result<Vec<f32>> {
2328 let qt = gguf.read_tensor(name, device).map_err(candle_to_ferrum)?;
2329 let dense = qt.dequantize(device).map_err(candle_to_ferrum)?;
2330 let flat = dense.flatten_all().map_err(candle_to_ferrum)?;
2331 flat.to_vec1::<f32>().map_err(candle_to_ferrum)
2332}
2333
2334fn candle_to_ferrum(e: candle_core::Error) -> FerrumError {
2335 FerrumError::model(format!("candle: {e}"))
2336}
2337
2338#[allow(dead_code)]
2341type _CandleResult<T> = CandleResult<T>;
2342
2343#[cfg(test)]
2344mod tests {
2345 use std::sync::atomic::Ordering;
2346
2347 use ferrum_kernels::backend::cpu::CpuBackend;
2348 use ferrum_kernels::backend::Backend;
2349 use ferrum_kernels::StackedExpertGgufLinear;
2350
2351 use super::{
2352 drain_moe_bucket_profile, pick_moe_block_size_with_config, ExpertStack,
2353 MoeDispatchRuntimeConfig, MOE_BUCKET_COMBINE_US, MOE_BUCKET_D2H_US, MOE_BUCKET_GATHER_US,
2354 MOE_BUCKET_GEMM1_US, MOE_BUCKET_GEMM3_US, MOE_BUCKET_LAYER_CALLS, MOE_BUCKET_PLAN_US,
2355 MOE_BUCKET_ROUTE_US, MOE_BUCKET_SILU_US, MOE_BUCKET_SYNC_US,
2356 };
2357
2358 struct FakeStackedGgufLinear {
2359 num_experts: usize,
2360 rows: usize,
2361 cols: usize,
2362 }
2363
2364 impl StackedExpertGgufLinear<CpuBackend> for FakeStackedGgufLinear {
2365 fn num_experts(&self) -> usize {
2366 self.num_experts
2367 }
2368
2369 fn n_rows(&self) -> usize {
2370 self.rows
2371 }
2372
2373 fn n_cols(&self) -> usize {
2374 self.cols
2375 }
2376
2377 fn as_any(&self) -> &dyn std::any::Any {
2378 self
2379 }
2380
2381 fn gemv_moe_id(
2382 &self,
2383 _ctx: &mut <CpuBackend as Backend>::Context,
2384 _a: &<CpuBackend as Backend>::Buffer,
2385 _ids: &<CpuBackend as Backend>::Buffer,
2386 _out: &mut <CpuBackend as Backend>::Buffer,
2387 _n_selected: usize,
2388 _src1_stride: usize,
2389 ) -> ferrum_types::Result<()> {
2390 unimplemented!("num_experts test does not dispatch kernels")
2391 }
2392
2393 fn gemv_moe_id_offset(
2394 &self,
2395 _ctx: &mut <CpuBackend as Backend>::Context,
2396 _a: &<CpuBackend as Backend>::Buffer,
2397 _a_offset: usize,
2398 _ids: &<CpuBackend as Backend>::Buffer,
2399 _ids_offset: usize,
2400 _out: &mut <CpuBackend as Backend>::Buffer,
2401 _n_selected: usize,
2402 _src1_stride: usize,
2403 ) -> ferrum_types::Result<()> {
2404 unimplemented!("num_experts test does not dispatch kernels")
2405 }
2406
2407 fn gemv_moe_id_gate_up_silu(
2408 &self,
2409 _ctx: &mut <CpuBackend as Backend>::Context,
2410 _a: &<CpuBackend as Backend>::Buffer,
2411 _other_up: &dyn StackedExpertGgufLinear<CpuBackend>,
2412 _ids: &<CpuBackend as Backend>::Buffer,
2413 _silu_out: &mut <CpuBackend as Backend>::Buffer,
2414 _n_selected: usize,
2415 ) -> ferrum_types::Result<()> {
2416 unimplemented!("num_experts test does not dispatch kernels")
2417 }
2418
2419 fn gemv_moe_id_batched(
2420 &self,
2421 _ctx: &mut <CpuBackend as Backend>::Context,
2422 _a: &<CpuBackend as Backend>::Buffer,
2423 _ids: &<CpuBackend as Backend>::Buffer,
2424 _out: &mut <CpuBackend as Backend>::Buffer,
2425 _m: usize,
2426 _top_k: usize,
2427 _src1_outer_stride: usize,
2428 _src1_inner_stride: usize,
2429 ) -> ferrum_types::Result<()> {
2430 unimplemented!("num_experts test does not dispatch kernels")
2431 }
2432
2433 fn gemv_moe_id_gate_up_silu_batched(
2434 &self,
2435 _ctx: &mut <CpuBackend as Backend>::Context,
2436 _a: &<CpuBackend as Backend>::Buffer,
2437 _other_up: &dyn StackedExpertGgufLinear<CpuBackend>,
2438 _ids: &<CpuBackend as Backend>::Buffer,
2439 _silu_out: &mut <CpuBackend as Backend>::Buffer,
2440 _m: usize,
2441 _top_k: usize,
2442 _src1_outer_stride: usize,
2443 _src1_inner_stride: usize,
2444 ) -> ferrum_types::Result<()> {
2445 unimplemented!("num_experts test does not dispatch kernels")
2446 }
2447
2448 fn gemm_moe_id(
2449 &self,
2450 _ctx: &mut <CpuBackend as Backend>::Context,
2451 _a: &<CpuBackend as Backend>::Buffer,
2452 _ids: &<CpuBackend as Backend>::Buffer,
2453 _tpe: &<CpuBackend as Backend>::Buffer,
2454 _out: &mut <CpuBackend as Backend>::Buffer,
2455 _ne11: usize,
2456 _top_k: usize,
2457 _max_per_expert: usize,
2458 _batch: usize,
2459 ) -> ferrum_types::Result<()> {
2460 unimplemented!("num_experts test does not dispatch kernels")
2461 }
2462
2463 fn gemm_moe_id_indirect(
2464 &self,
2465 _ctx: &mut <CpuBackend as Backend>::Context,
2466 _src1: &<CpuBackend as Backend>::Buffer,
2467 _ids: &<CpuBackend as Backend>::Buffer,
2468 _tpe: &<CpuBackend as Backend>::Buffer,
2469 _out: &mut <CpuBackend as Backend>::Buffer,
2470 _args_buf: &<CpuBackend as Backend>::Buffer,
2471 _ne11: usize,
2472 _top_k: usize,
2473 _max_per_expert: usize,
2474 _batch: usize,
2475 ) -> ferrum_types::Result<()> {
2476 unimplemented!("num_experts test does not dispatch kernels")
2477 }
2478 }
2479
2480 fn fake_stacked(num_experts: usize) -> Box<dyn StackedExpertGgufLinear<CpuBackend>> {
2481 Box::new(FakeStackedGgufLinear {
2482 num_experts,
2483 rows: 4,
2484 cols: 4,
2485 })
2486 }
2487
2488 #[test]
2489 fn expert_stack_num_experts_uses_stacked_fast_path_count() {
2490 let experts = ExpertStack::<CpuBackend> {
2491 gate_up: Vec::new(),
2492 down: Vec::new(),
2493 gate_stacked: Some(fake_stacked(7)),
2494 up_stacked: Some(fake_stacked(7)),
2495 down_stacked: Some(fake_stacked(7)),
2496 gate_up_marlin_stack: None,
2497 down_marlin_stack: None,
2498 };
2499
2500 assert_eq!(experts.num_experts(), 7);
2501 }
2502
2503 #[test]
2504 fn moe_dispatch_runtime_config_parses_m3_startup_knobs() {
2505 let config = MoeDispatchRuntimeConfig::from_env_vars([
2506 ("FERRUM_MOE_PROFILE", "0"),
2507 ("FERRUM_DECODE_OP_PROFILE", "true"),
2508 ("FERRUM_VLLM_MOE_ZERO_WS", "1"),
2509 ("FERRUM_VLLM_MOE_PAIR_IDS", "1"),
2510 ("FERRUM_MOE_LOAD_TRACE", ""),
2511 ("FERRUM_MOE_BLOCK_SIZE", "8"),
2512 ("FERRUM_MOE_LARGE_M_BLOCK_SIZE", "64"),
2513 ("FERRUM_MOE_LARGE_M_MIN_PAIRS", "2048"),
2514 ("FERRUM_VLLM_MOE", "1"),
2515 ("FERRUM_MOE_HOST_ROUTE", "1"),
2516 ]);
2517
2518 assert!(config.moe_profile);
2519 assert!(config.decode_op_profile);
2520 assert!(config.vllm_moe_zero_ws);
2521 assert!(config.vllm_moe_pair_ids);
2522 assert!(config.moe_load_trace);
2523 assert_eq!(config.moe_block_size, Some(8));
2524 assert_eq!(config.moe_large_m_block_size, Some(64));
2525 assert_eq!(config.moe_large_m_min_pairs, 2048);
2526 assert!(config.vllm_moe);
2527 assert!(config.moe_host_route);
2528 }
2529
2530 #[test]
2531 fn drain_moe_bucket_profile_returns_and_clears_counters() {
2532 let _ = drain_moe_bucket_profile();
2533 MOE_BUCKET_LAYER_CALLS.store(2, Ordering::Relaxed);
2534 MOE_BUCKET_SYNC_US.store(3, Ordering::Relaxed);
2535 MOE_BUCKET_D2H_US.store(5, Ordering::Relaxed);
2536 MOE_BUCKET_ROUTE_US.store(7, Ordering::Relaxed);
2537 MOE_BUCKET_PLAN_US.store(11, Ordering::Relaxed);
2538 MOE_BUCKET_GATHER_US.store(13, Ordering::Relaxed);
2539 MOE_BUCKET_GEMM1_US.store(17, Ordering::Relaxed);
2540 MOE_BUCKET_SILU_US.store(19, Ordering::Relaxed);
2541 MOE_BUCKET_GEMM3_US.store(23, Ordering::Relaxed);
2542 MOE_BUCKET_COMBINE_US.store(29, Ordering::Relaxed);
2543
2544 let snapshot = drain_moe_bucket_profile();
2545 assert_eq!(snapshot.layers, 2);
2546 assert_eq!(snapshot.total_us(), 127);
2547 assert!(snapshot.has_layers());
2548
2549 let cleared = drain_moe_bucket_profile();
2550 assert_eq!(cleared.layers, 0);
2551 assert_eq!(cleared.total_us(), 0);
2552 assert!(!cleared.has_layers());
2553 }
2554
2555 #[test]
2556 fn moe_dispatch_runtime_config_bounds_invalid_block_values() {
2557 let config = MoeDispatchRuntimeConfig::from_env_vars([
2558 ("FERRUM_MOE_BLOCK_SIZE", "12"),
2559 ("FERRUM_MOE_LARGE_M_BLOCK_SIZE", "128"),
2560 ("FERRUM_MOE_LARGE_M_MIN_PAIRS", "bad"),
2561 ("FERRUM_VLLM_MOE_ZERO_WS", "true"),
2562 ("FERRUM_MOE_HOST_ROUTE", "0"),
2563 ]);
2564
2565 assert_eq!(config.moe_block_size, None);
2566 assert_eq!(config.moe_large_m_block_size, None);
2567 assert_eq!(config.moe_large_m_min_pairs, 1024);
2568 assert!(!config.vllm_moe_zero_ws);
2569 assert!(!config.moe_host_route);
2570 }
2571
2572 #[test]
2573 fn device_route_large_m_block_size_is_thresholded() {
2574 let config = MoeDispatchRuntimeConfig::from_env_vars([
2575 ("FERRUM_MOE_LARGE_M_BLOCK_SIZE", "64"),
2576 ("FERRUM_MOE_LARGE_M_MIN_PAIRS", "1024"),
2577 ]);
2578
2579 assert_eq!(
2580 pick_moe_block_size_with_config(&config, None, 128, true, 256),
2581 16
2582 );
2583 assert_eq!(
2584 pick_moe_block_size_with_config(&config, None, 128, true, 1024),
2585 64
2586 );
2587 }
2588
2589 #[test]
2590 fn global_moe_block_size_override_still_wins() {
2591 let config = MoeDispatchRuntimeConfig::from_env_vars([
2592 ("FERRUM_MOE_BLOCK_SIZE", "32"),
2593 ("FERRUM_MOE_LARGE_M_BLOCK_SIZE", "64"),
2594 ("FERRUM_MOE_LARGE_M_MIN_PAIRS", "1024"),
2595 ]);
2596
2597 assert_eq!(
2598 pick_moe_block_size_with_config(&config, None, 128, true, 2048),
2599 32
2600 );
2601 }
2602}