use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OverflowPolicy {
TokenDrop,
NoDrop,
}
#[derive(Debug, Clone)]
pub struct MoeConfig {
pub num_experts: usize,
pub top_k: usize,
pub capacity_factor: f32,
pub overflow_policy: OverflowPolicy,
}
impl MoeConfig {
pub fn new(num_experts: usize, top_k: usize) -> Self {
assert!(num_experts > 0, "num_experts must be > 0");
assert!(top_k > 0, "top_k must be > 0");
assert!(top_k <= num_experts, "top_k must be <= num_experts");
Self {
num_experts,
top_k,
capacity_factor: 1.0,
overflow_policy: OverflowPolicy::NoDrop,
}
}
pub fn with_capacity_factor(mut self, factor: f32) -> Self {
assert!(factor > 0.0, "capacity_factor must be > 0");
self.capacity_factor = factor;
self
}
pub fn with_overflow_policy(mut self, policy: OverflowPolicy) -> Self {
self.overflow_policy = policy;
self
}
}
#[derive(Debug, Clone)]
pub struct TokenAssignment {
pub expert_indices: Vec<usize>,
pub expert_weights: Vec<f32>,
pub dropped: bool,
}
#[derive(Debug, Clone)]
pub struct ExpertBatch {
pub expert_id: usize,
pub token_indices: Vec<usize>,
pub weights: Vec<f32>,
}
#[derive(Debug, Clone)]
pub struct RoutingResult {
assignments: Vec<TokenAssignment>,
expert_batches: Vec<ExpertBatch>,
num_dropped: usize,
top_k: usize,
}
impl RoutingResult {
pub fn num_tokens(&self) -> usize {
self.assignments.len()
}
pub fn top_k(&self) -> usize {
self.top_k
}
pub fn assignments(&self) -> &[TokenAssignment] {
&self.assignments
}
pub fn expert_batches(&self) -> &[ExpertBatch] {
&self.expert_batches
}
pub fn num_dropped(&self) -> usize {
self.num_dropped
}
pub fn expert_load(&self) -> HashMap<usize, usize> {
let mut load = HashMap::new();
for batch in &self.expert_batches {
load.insert(batch.expert_id, batch.token_indices.len());
}
load
}
}
#[derive(Debug, Clone)]
pub struct MoeRouter {
config: MoeConfig,
}
impl MoeRouter {
pub fn new(config: MoeConfig) -> Self {
Self { config }
}
pub fn route(&self, logits: &[Vec<f32>]) -> RoutingResult {
let batch_size = logits.len();
let num_experts = self.config.num_experts;
let top_k = self.config.top_k;
if batch_size == 0 {
return RoutingResult {
assignments: Vec::new(),
expert_batches: Vec::new(),
num_dropped: 0,
top_k,
};
}
let mut assignments = Vec::with_capacity(batch_size);
for row in logits {
assert_eq!(
row.len(),
num_experts,
"logits row length must equal num_experts"
);
let (indices, weights) = self.top_k_softmax(row, top_k);
assignments.push(TokenAssignment {
expert_indices: indices,
expert_weights: weights,
dropped: false,
});
}
let num_dropped = match self.config.overflow_policy {
OverflowPolicy::TokenDrop => self.apply_capacity_control(&mut assignments, batch_size),
OverflowPolicy::NoDrop => 0,
};
let expert_batches = self.build_expert_batches(&assignments);
RoutingResult {
assignments,
expert_batches,
num_dropped,
top_k,
}
}
fn top_k_softmax(&self, logits: &[f32], k: usize) -> (Vec<usize>, Vec<f32>) {
let mut indexed: Vec<(usize, f32)> = logits.iter().copied().enumerate().collect();
indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
indexed.truncate(k);
let indices: Vec<usize> = indexed.iter().map(|&(i, _)| i).collect();
let top_logits: Vec<f32> = indexed.iter().map(|&(_, v)| v).collect();
let max_logit = top_logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let exps: Vec<f32> = top_logits.iter().map(|&v| (v - max_logit).exp()).collect();
let sum: f32 = exps.iter().sum();
let weights: Vec<f32> = if sum > 0.0 {
exps.iter().map(|&e| e / sum).collect()
} else {
vec![1.0 / k as f32; k]
};
(indices, weights)
}
fn apply_capacity_control(
&self,
assignments: &mut [TokenAssignment],
batch_size: usize,
) -> usize {
let num_experts = self.config.num_experts;
let top_k = self.config.top_k;
let avg_tokens_per_expert =
((batch_size * top_k) as f32 / num_experts as f32).ceil() as usize;
let expert_capacity =
(self.config.capacity_factor * avg_tokens_per_expert as f32).ceil() as usize;
let expert_capacity = expert_capacity.max(1);
let mut expert_counts = vec![0usize; num_experts];
let mut dropped = 0;
for assign in assignments.iter_mut() {
let mut new_indices = Vec::with_capacity(top_k);
let mut new_weights = Vec::with_capacity(top_k);
for (&eidx, &w) in assign.expert_indices.iter().zip(&assign.expert_weights) {
if expert_counts[eidx] < expert_capacity {
expert_counts[eidx] += 1;
new_indices.push(eidx);
new_weights.push(w);
} else {
dropped += 1;
assign.dropped = true;
}
}
if new_indices.len() < assign.expert_indices.len() {
let wsum: f32 = new_weights.iter().sum();
if wsum > 0.0 {
for w in &mut new_weights {
*w /= wsum;
}
}
}
assign.expert_indices = new_indices;
assign.expert_weights = new_weights;
}
dropped
}
fn build_expert_batches(&self, assignments: &[TokenAssignment]) -> Vec<ExpertBatch> {
let mut expert_map: HashMap<usize, (Vec<usize>, Vec<f32>)> = HashMap::new();
for (token_idx, assign) in assignments.iter().enumerate() {
for (&expert_id, &weight) in assign.expert_indices.iter().zip(&assign.expert_weights) {
let entry = expert_map.entry(expert_id).or_default();
entry.0.push(token_idx);
entry.1.push(weight);
}
}
let mut batches: Vec<ExpertBatch> = expert_map
.into_iter()
.map(|(expert_id, (token_indices, weights))| ExpertBatch {
expert_id,
token_indices,
weights,
})
.collect();
batches.sort_by_key(|b| b.expert_id);
batches
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_logits(rows: &[&[f32]]) -> Vec<Vec<f32>> {
rows.iter().map(|r| r.to_vec()).collect()
}
#[test]
fn basic_top2_routing() {
let router = MoeRouter::new(MoeConfig::new(4, 2));
let logits = make_logits(&[
&[3.0, 1.0, 0.0, 0.0], &[0.0, 0.0, 2.0, 4.0], ]);
let result = router.route(&logits);
assert_eq!(result.num_tokens(), 2);
assert_eq!(result.top_k(), 2);
let a0 = &result.assignments()[0];
assert!(a0.expert_indices.contains(&0));
assert!(a0.expert_indices.contains(&1));
let a1 = &result.assignments()[1];
assert!(a1.expert_indices.contains(&3));
assert!(a1.expert_indices.contains(&2));
}
#[test]
fn weights_sum_to_one() {
let router = MoeRouter::new(MoeConfig::new(8, 2));
let logits = make_logits(&[&[1.0, 2.0, 3.0, 0.5, 0.1, 0.0, 0.0, 0.0]]);
let result = router.route(&logits);
let wsum: f32 = result.assignments()[0].expert_weights.iter().sum();
assert!((wsum - 1.0).abs() < 1e-5, "weights sum = {wsum}");
}
#[test]
fn token_drop_capacity() {
let config = MoeConfig::new(2, 1)
.with_capacity_factor(1.0)
.with_overflow_policy(OverflowPolicy::TokenDrop);
let router = MoeRouter::new(config);
let logits = make_logits(&[&[10.0, 0.0], &[10.0, 0.0], &[10.0, 0.0], &[10.0, 0.0]]);
let result = router.route(&logits);
assert!(result.num_dropped() > 0);
}
#[test]
fn no_drop_policy() {
let config = MoeConfig::new(2, 1).with_overflow_policy(OverflowPolicy::NoDrop);
let router = MoeRouter::new(config);
let logits = make_logits(&[&[10.0, 0.0], &[10.0, 0.0], &[10.0, 0.0]]);
let result = router.route(&logits);
assert_eq!(result.num_dropped(), 0);
let load = result.expert_load();
assert_eq!(load[&0], 3);
}
#[test]
fn expert_batches_built() {
let router = MoeRouter::new(MoeConfig::new(4, 1));
let logits = make_logits(&[
&[5.0, 0.0, 0.0, 0.0], &[0.0, 5.0, 0.0, 0.0], &[0.0, 0.0, 0.0, 5.0], &[5.0, 0.0, 0.0, 0.0], ]);
let result = router.route(&logits);
let batches = result.expert_batches();
assert_eq!(batches.len(), 3);
let e0 = batches.iter().find(|b| b.expert_id == 0).unwrap();
assert_eq!(e0.token_indices, vec![0, 3]);
}
#[test]
fn empty_batch() {
let router = MoeRouter::new(MoeConfig::new(4, 2));
let result = router.route(&[]);
assert_eq!(result.num_tokens(), 0);
assert!(result.expert_batches().is_empty());
}
#[test]
fn top_k_equals_num_experts() {
let router = MoeRouter::new(MoeConfig::new(3, 3));
let logits = make_logits(&[&[1.0, 2.0, 3.0]]);
let result = router.route(&logits);
assert_eq!(result.assignments()[0].expert_indices.len(), 3);
}
#[test]
fn expert_load_distribution() {
let router = MoeRouter::new(MoeConfig::new(4, 2));
let logits = make_logits(&[
&[5.0, 4.0, 0.0, 0.0],
&[0.0, 4.0, 5.0, 0.0],
&[0.0, 0.0, 5.0, 4.0],
&[4.0, 0.0, 0.0, 5.0],
]);
let result = router.route(&logits);
let load = result.expert_load();
for expert_id in 0..4 {
assert_eq!(load[&expert_id], 2, "expert {expert_id} load mismatch");
}
}
#[test]
#[should_panic(expected = "top_k must be <= num_experts")]
fn top_k_exceeds_experts() {
MoeConfig::new(4, 5);
}
#[test]
fn dropped_tokens_flag() {
let config = MoeConfig::new(2, 1)
.with_capacity_factor(0.5) .with_overflow_policy(OverflowPolicy::TokenDrop);
let router = MoeRouter::new(config);
let logits = make_logits(&[&[10.0, 0.0], &[10.0, 0.0], &[10.0, 0.0], &[10.0, 0.0]]);
let result = router.route(&logits);
let any_dropped = result.assignments().iter().any(|a| a.dropped);
assert!(any_dropped);
}
}