use crate::types::Expert;
use async_trait::async_trait;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct RouteResult {
pub expert_index: usize,
pub confidence: f64,
pub alternatives: Vec<usize>,
pub metadata: std::collections::HashMap<String, String>,
}
impl RouteResult {
pub fn new(expert_index: usize) -> Self {
Self {
expert_index,
confidence: 1.0,
alternatives: Vec::new(),
metadata: std::collections::HashMap::new(),
}
}
pub fn with_confidence(mut self, confidence: f64) -> Self {
self.confidence = confidence.clamp(0.0, 1.0);
self
}
pub fn with_alternatives(mut self, alternatives: Vec<usize>) -> Self {
self.alternatives = alternatives;
self
}
}
#[async_trait]
pub trait MoERouter: Send + Sync {
async fn route(&self, input: &str, num_experts: usize) -> RouteResult;
async fn route_with_experts(&self, input: &str, experts: &[Expert]) -> RouteResult {
let available: Vec<_> = experts.iter().filter(|e| e.available()).collect();
if available.is_empty() {
self.route(input, experts.len()).await
} else {
let result = self.route(input, available.len()).await;
RouteResult::new(available[result.expert_index].index)
.with_confidence(result.confidence)
}
}
fn name(&self) -> &str {
"custom"
}
}
#[derive(Debug, Clone, Default)]
pub struct DefaultMoERouter {
virtual_shards: usize,
}
impl DefaultMoERouter {
pub fn new() -> Self {
Self { virtual_shards: 1 }
}
pub fn with_virtual_shards(mut self, shards: usize) -> Self {
self.virtual_shards = shards.max(1);
self
}
fn hash_input(&self, input: &str) -> u64 {
let mut hasher = DefaultHasher::new();
input.hash(&mut hasher);
hasher.finish()
}
}
#[async_trait]
impl MoERouter for DefaultMoERouter {
async fn route(&self, input: &str, num_experts: usize) -> RouteResult {
if num_experts == 0 {
return RouteResult::new(0);
}
let hash = self.hash_input(input);
let expert_index = (hash % num_experts as u64) as usize;
RouteResult::new(expert_index).with_confidence(1.0)
}
fn name(&self) -> &str {
"default-hash"
}
}
#[derive(Debug, Clone)]
pub struct LoadAwareMoERouter {
affinity_threshold: f64,
fallback: DefaultMoERouter,
}
impl LoadAwareMoERouter {
pub fn new() -> Self {
Self {
affinity_threshold: 0.1,
fallback: DefaultMoERouter::new(),
}
}
pub fn with_affinity_threshold(mut self, threshold: f64) -> Self {
self.affinity_threshold = threshold.clamp(0.0, 1.0);
self
}
}
impl Default for LoadAwareMoERouter {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl MoERouter for LoadAwareMoERouter {
async fn route(&self, input: &str, num_experts: usize) -> RouteResult {
self.fallback.route(input, num_experts).await
}
async fn route_with_experts(&self, input: &str, experts: &[Expert]) -> RouteResult {
let available: Vec<_> = experts.iter().filter(|e| e.available()).collect();
if available.is_empty() {
return RouteResult::new(0);
}
let min_load = available
.iter()
.map(|e| e.load)
.fold(f64::INFINITY, f64::min);
let affinity_result = self.fallback.route(input, experts.len()).await;
let affinity_expert = experts.get(affinity_result.expert_index);
if let Some(expert) = affinity_expert {
if expert.available() && (expert.load - min_load) < self.affinity_threshold {
return RouteResult::new(expert.index).with_confidence(0.9);
}
}
let selected = available
.iter()
.min_by(|a, b| a.load.partial_cmp(&b.load).unwrap_or(std::cmp::Ordering::Equal))
.expect("available is non-empty, checked above");
let alternatives: Vec<_> = available
.iter()
.filter(|e| e.index != selected.index)
.take(2)
.map(|e| e.index)
.collect();
RouteResult::new(selected.index)
.with_confidence(0.8)
.with_alternatives(alternatives)
}
fn name(&self) -> &str {
"load-aware"
}
}
#[derive(Debug)]
pub struct RoundRobinMoERouter {
counter: std::sync::atomic::AtomicUsize,
}
impl RoundRobinMoERouter {
pub fn new() -> Self {
Self {
counter: std::sync::atomic::AtomicUsize::new(0),
}
}
}
impl Default for RoundRobinMoERouter {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl MoERouter for RoundRobinMoERouter {
async fn route(&self, _input: &str, num_experts: usize) -> RouteResult {
if num_experts == 0 {
return RouteResult::new(0);
}
let count = self
.counter
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let expert_index = count % num_experts;
RouteResult::new(expert_index)
}
fn name(&self) -> &str {
"round-robin"
}
}
#[derive(Debug, Clone)]
pub struct GpuAwareMoERouter {
min_memory_mb: u64,
prefer_tensor_cores: bool,
fallback: LoadAwareMoERouter,
}
impl GpuAwareMoERouter {
pub fn new() -> Self {
Self {
min_memory_mb: 0,
prefer_tensor_cores: false,
fallback: LoadAwareMoERouter::new(),
}
}
pub fn with_min_memory(mut self, memory_mb: u64) -> Self {
self.min_memory_mb = memory_mb;
self
}
pub fn prefer_tensor_cores(mut self, prefer: bool) -> Self {
self.prefer_tensor_cores = prefer;
self
}
}
impl Default for GpuAwareMoERouter {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl MoERouter for GpuAwareMoERouter {
async fn route(&self, input: &str, num_experts: usize) -> RouteResult {
self.fallback.route(input, num_experts).await
}
async fn route_with_experts(&self, input: &str, experts: &[Expert]) -> RouteResult {
let gpu_experts: Vec<_> = experts
.iter()
.filter(|e| {
e.available() && e.gpu.as_ref().map(|g| {
g.available() && g.available_memory_mb() >= self.min_memory_mb
}).unwrap_or(false)
})
.collect();
if gpu_experts.is_empty() {
return self.fallback.route_with_experts(input, experts).await;
}
let candidates: Vec<_> = if self.prefer_tensor_cores {
let tensor_experts: Vec<_> = gpu_experts
.iter()
.filter(|e| e.gpu.as_ref().map(|g| g.tensor_cores).unwrap_or(false))
.copied()
.collect();
if tensor_experts.is_empty() { gpu_experts } else { tensor_experts }
} else {
gpu_experts
};
let selected = candidates
.iter()
.min_by(|a, b| {
let a_gpu = a.gpu.as_ref().unwrap();
let b_gpu = b.gpu.as_ref().unwrap();
a_gpu.utilization.partial_cmp(&b_gpu.utilization)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| b_gpu.available_memory_mb().cmp(&a_gpu.available_memory_mb()))
})
.expect("candidates is non-empty");
let alternatives: Vec<_> = candidates
.iter()
.filter(|e| e.index != selected.index)
.take(2)
.map(|e| e.index)
.collect();
RouteResult::new(selected.index)
.with_confidence(0.9)
.with_alternatives(alternatives)
}
fn name(&self) -> &str {
"gpu-aware"
}
}
#[derive(Debug, Clone)]
pub struct VersionAwareMoERouter {
target_version: Option<String>,
canary_percent: u8,
canary_version: Option<String>,
fallback: LoadAwareMoERouter,
}
impl VersionAwareMoERouter {
pub fn new() -> Self {
Self {
target_version: None,
canary_percent: 0,
canary_version: None,
fallback: LoadAwareMoERouter::new(),
}
}
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.target_version = Some(version.into());
self
}
pub fn with_canary(mut self, version: impl Into<String>, percent: u8) -> Self {
self.canary_version = Some(version.into());
self.canary_percent = percent.min(100);
self
}
}
impl Default for VersionAwareMoERouter {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl MoERouter for VersionAwareMoERouter {
async fn route(&self, input: &str, num_experts: usize) -> RouteResult {
self.fallback.route(input, num_experts).await
}
async fn route_with_experts(&self, input: &str, experts: &[Expert]) -> RouteResult {
let use_canary = if self.canary_percent > 0 && self.canary_version.is_some() {
let hash = {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
input.hash(&mut hasher);
hasher.finish()
};
(hash % 100) < self.canary_percent as u64
} else {
false
};
let target = if use_canary {
self.canary_version.as_ref()
} else {
self.target_version.as_ref()
};
let versioned_experts: Vec<_> = if let Some(version) = target {
experts
.iter()
.filter(|e| e.available() && e.model_version.as_ref() == Some(version))
.collect()
} else {
experts.iter().filter(|e| e.available()).collect()
};
if versioned_experts.is_empty() {
return self.fallback.route_with_experts(input, experts).await;
}
let selected = versioned_experts
.iter()
.min_by(|a, b| a.load.partial_cmp(&b.load).unwrap_or(std::cmp::Ordering::Equal))
.expect("versioned_experts is non-empty");
RouteResult::new(selected.index)
.with_confidence(if use_canary { 0.7 } else { 0.9 })
}
fn name(&self) -> &str {
"version-aware"
}
}
pub type BoxedMoERouter = Arc<dyn MoERouter>;
pub fn default_router() -> BoxedMoERouter {
Arc::new(DefaultMoERouter::new())
}
pub fn load_aware_router() -> BoxedMoERouter {
Arc::new(LoadAwareMoERouter::new())
}
pub fn gpu_aware_router() -> BoxedMoERouter {
Arc::new(GpuAwareMoERouter::new())
}
pub fn version_aware_router() -> BoxedMoERouter {
Arc::new(VersionAwareMoERouter::new())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::NodeId;
#[tokio::test]
async fn test_default_router_consistency() {
let router = DefaultMoERouter::new();
let result1 = router.route("test-input", 8).await;
let result2 = router.route("test-input", 8).await;
assert_eq!(result1.expert_index, result2.expert_index);
}
#[tokio::test]
async fn test_default_router_distribution() {
let router = DefaultMoERouter::new();
let mut counts = vec![0usize; 4];
for i in 0..1000 {
let input = format!("input-{}", i);
let result = router.route(&input, 4).await;
counts[result.expert_index] += 1;
}
for count in counts {
assert!(count > 150 && count < 350, "Uneven distribution: {}", count);
}
}
#[tokio::test]
async fn test_load_aware_router() {
let router = LoadAwareMoERouter::new();
let experts = vec![
Expert::new(0, NodeId::new()),
{
let mut e = Expert::new(1, NodeId::new());
e.update_load(0.9);
e
},
Expert::new(2, NodeId::new()),
];
let result = router.route_with_experts("test", &experts).await;
assert_ne!(result.expert_index, 1);
}
#[tokio::test]
async fn test_round_robin_router() {
let router = RoundRobinMoERouter::new();
let r0 = router.route("a", 3).await;
let r1 = router.route("b", 3).await;
let r2 = router.route("c", 3).await;
let r3 = router.route("d", 3).await;
assert_eq!(r0.expert_index, 0);
assert_eq!(r1.expert_index, 1);
assert_eq!(r2.expert_index, 2);
assert_eq!(r3.expert_index, 0);
}
}