use ferrox_core::matmul::swiglu;
use ferrox_core::weight_matrix::WeightMatrix;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExpertPlacement {
Cpu,
GpuDevice(u32),
}
#[derive(Debug, Clone)]
pub struct MoeLayerConfig {
pub n_experts: usize,
pub n_experts_active: usize,
pub n_shared_experts: usize,
pub hidden_dim: usize,
pub expert_ffn_dim: usize,
pub gating: GatingFunction,
pub norm_topk_prob: bool,
pub expert_group_count: Option<usize>,
pub expert_group_used_count: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct RoutingDecision {
pub expert_ids: Vec<usize>,
pub weights: Vec<f32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GatingFunction {
Softmax,
Sigmoid,
SqrtSoftplus,
}
fn sigmoid(x: f32) -> f32 {
1.0 / (1.0 + (-x).exp())
}
fn sqrt_softplus(x: f32) -> f32 {
let softplus = x.max(0.0) + (-x.abs()).exp().ln_1p();
softplus.sqrt()
}
pub fn route_top_k(
logits: &[f32],
k: usize,
gating: GatingFunction,
norm_topk_prob: bool,
) -> RoutingDecision {
match gating {
GatingFunction::Softmax => route_top_k_softmax(logits, k, norm_topk_prob),
GatingFunction::Sigmoid => route_top_k_sigmoid(logits, k),
GatingFunction::SqrtSoftplus => route_top_k_sqrtsoftplus(logits, k, norm_topk_prob),
}
}
pub fn route_top_k_grouped(
logits: &[f32],
n_groups: usize,
k_per_group: usize,
total_k: usize,
gating: GatingFunction,
norm_topk_prob: bool,
) -> RoutingDecision {
if n_groups <= 1 || !logits.len().is_multiple_of(n_groups) {
return route_top_k(logits, total_k, gating, norm_topk_prob);
}
let group_size = logits.len() / n_groups;
let mut selected: Vec<(usize, f32)> = Vec::new();
for g in 0..n_groups {
let start = g * group_size;
let slice = &logits[start..start + group_size];
let local = route_top_k(slice, k_per_group.min(group_size), gating, false);
for (i, &expert) in local.expert_ids.iter().enumerate() {
selected.push((start + expert, local.weights[i]));
}
}
selected.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
selected.truncate(total_k.min(selected.len()));
let mut weights: Vec<f32> = selected.iter().map(|(_, w)| *w).collect();
if norm_topk_prob {
let sum: f32 = weights.iter().sum();
if sum > 0.0 {
for w in weights.iter_mut() {
*w /= sum;
}
}
}
RoutingDecision {
expert_ids: selected.into_iter().map(|(i, _)| i).collect(),
weights,
}
}
pub fn route_top_k_sqrtsoftplus(logits: &[f32], k: usize, norm_topk_prob: bool) -> RoutingDecision {
let scores: Vec<f32> = logits.iter().map(|&l| sqrt_softplus(l)).collect();
let mut idx: Vec<usize> = (0..scores.len()).collect();
idx.sort_unstable_by(|&a, &b| scores[b].partial_cmp(&scores[a]).unwrap());
let top = &idx[..k.min(idx.len())];
let mut weights: Vec<f32> = top.iter().map(|&i| scores[i]).collect();
if norm_topk_prob {
let sum: f32 = weights.iter().sum::<f32>() + 1e-20;
for w in weights.iter_mut() {
*w /= sum;
}
}
RoutingDecision {
expert_ids: top.to_vec(),
weights,
}
}
pub fn route_top_k_sqrtsoftplus_with_bias(
logits: &[f32],
bias: &[f32],
k: usize,
renormalize: bool,
scaling_factor: f32,
) -> RoutingDecision {
assert_eq!(logits.len(), bias.len());
let scores: Vec<f32> = logits.iter().map(|&l| sqrt_softplus(l)).collect();
let scores_for_choice: Vec<f32> = scores.iter().zip(bias.iter()).map(|(s, b)| s + b).collect();
let mut idx: Vec<usize> = (0..scores.len()).collect();
idx.sort_unstable_by(|&a, &b| {
scores_for_choice[b]
.partial_cmp(&scores_for_choice[a])
.unwrap()
});
let top = &idx[..k.min(idx.len())];
let mut weights: Vec<f32> = top.iter().map(|&i| scores[i]).collect();
if k > 1 && renormalize {
let sum: f32 = weights.iter().sum::<f32>() + 1e-20;
for w in weights.iter_mut() {
*w /= sum;
}
}
for w in weights.iter_mut() {
*w *= scaling_factor;
}
RoutingDecision {
expert_ids: top.to_vec(),
weights,
}
}
pub fn route_hash(
hash_expert_ids: &[usize],
logits: &[f32],
renormalize: bool,
scaling_factor: f32,
) -> RoutingDecision {
let mut weights: Vec<f32> = hash_expert_ids
.iter()
.map(|&e| sqrt_softplus(logits[e]))
.collect();
if hash_expert_ids.len() > 1 && renormalize {
let sum: f32 = weights.iter().sum::<f32>() + 1e-20;
for w in weights.iter_mut() {
*w /= sum;
}
}
for w in weights.iter_mut() {
*w *= scaling_factor;
}
RoutingDecision {
expert_ids: hash_expert_ids.to_vec(),
weights,
}
}
pub fn route_top_k_softmax(logits: &[f32], k: usize, norm_topk_prob: bool) -> RoutingDecision {
let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let exps: Vec<f32> = logits.iter().map(|&l| (l - max).exp()).collect();
let sum: f32 = exps.iter().sum();
let probs: Vec<f32> = exps.iter().map(|e| e / sum).collect();
let mut idx: Vec<usize> = (0..probs.len()).collect();
idx.sort_unstable_by(|&a, &b| probs[b].partial_cmp(&probs[a]).unwrap());
let top = &idx[..k.min(idx.len())];
let mut weights: Vec<f32> = top.iter().map(|&i| probs[i]).collect();
if norm_topk_prob {
let top_sum: f32 = weights.iter().sum();
for w in weights.iter_mut() {
*w /= top_sum;
}
}
RoutingDecision {
expert_ids: top.to_vec(),
weights,
}
}
pub fn route_top_k_sigmoid(logits: &[f32], k: usize) -> RoutingDecision {
let scores: Vec<f32> = logits.iter().map(|&l| sigmoid(l)).collect();
let mut idx: Vec<usize> = (0..scores.len()).collect();
idx.sort_unstable_by(|&a, &b| scores[b].partial_cmp(&scores[a]).unwrap());
let top = &idx[..k.min(idx.len())];
let sum: f32 = top.iter().map(|&i| scores[i]).sum();
let weights: Vec<f32> = if sum > 0.0 {
top.iter().map(|&i| scores[i] / sum).collect()
} else {
vec![1.0 / top.len() as f32; top.len()]
};
RoutingDecision {
expert_ids: top.to_vec(),
weights,
}
}
pub fn route_top_k_sigmoid_with_bias(
logits: &[f32],
bias: &[f32],
k: usize,
renormalize: bool,
scaling_factor: f32,
) -> RoutingDecision {
assert_eq!(logits.len(), bias.len());
let scores: Vec<f32> = logits.iter().map(|&l| sigmoid(l)).collect();
let scores_for_choice: Vec<f32> = scores.iter().zip(bias.iter()).map(|(s, b)| s + b).collect();
let mut idx: Vec<usize> = (0..scores.len()).collect();
idx.sort_unstable_by(|&a, &b| {
scores_for_choice[b]
.partial_cmp(&scores_for_choice[a])
.unwrap()
});
let top = &idx[..k.min(idx.len())];
let mut weights: Vec<f32> = top.iter().map(|&i| scores[i]).collect();
if k > 1 && renormalize {
let sum: f32 = weights.iter().sum::<f32>() + 1e-20;
for w in weights.iter_mut() {
*w /= sum;
}
}
for w in weights.iter_mut() {
*w *= scaling_factor;
}
RoutingDecision {
expert_ids: top.to_vec(),
weights,
}
}
#[derive(Debug, Clone)]
pub struct PlacementPlan {
pub default_placement: ExpertPlacement,
pub overrides: std::collections::HashMap<usize, ExpertPlacement>,
}
impl PlacementPlan {
pub fn all_cpu(n_experts: usize) -> Self {
PlacementPlan {
default_placement: ExpertPlacement::Cpu,
overrides: (0..n_experts).map(|i| (i, ExpertPlacement::Cpu)).collect(),
}
}
pub fn hot_experts_on_gpu(n_experts: usize, n_gpu_resident: usize) -> Self {
let mut overrides = std::collections::HashMap::new();
for i in 0..n_experts.min(n_gpu_resident) {
overrides.insert(i, ExpertPlacement::GpuDevice(0));
}
PlacementPlan {
default_placement: ExpertPlacement::Cpu,
overrides,
}
}
pub fn from_budget(
expert_bytes: &[usize],
activation_counts: Option<&[u64]>,
vram_budget_bytes: u64,
) -> Self {
let n = expert_bytes.len();
let mut order: Vec<usize> = (0..n).collect();
if let Some(counts) = activation_counts {
if counts.len() == n {
order.sort_by(|&a, &b| counts[b].cmp(&counts[a]).then(a.cmp(&b)));
}
}
let mut overrides = std::collections::HashMap::new();
let mut used: u64 = 0;
for idx in order {
let size = expert_bytes[idx] as u64;
if size == 0 || used + size > vram_budget_bytes {
continue;
}
used += size;
overrides.insert(idx, ExpertPlacement::GpuDevice(0));
}
PlacementPlan {
default_placement: ExpertPlacement::Cpu,
overrides,
}
}
pub fn plan_layers_against_global_budget(
expert_bytes_per_layer: &[Vec<usize>],
activation_counts_per_layer: Option<&[Vec<u64>]>,
vram_budget_bytes: u64,
) -> ResidencyPlan {
let mut candidates: Vec<(u64, usize, usize)> = Vec::new(); for (l, sizes) in expert_bytes_per_layer.iter().enumerate() {
for e in 0..sizes.len() {
let count = activation_counts_per_layer
.and_then(|cs| cs.get(l))
.and_then(|c| c.get(e))
.copied()
.unwrap_or(0);
candidates.push((count, l, e));
}
}
candidates.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
let mut layer_overrides: Vec<std::collections::HashMap<usize, ExpertPlacement>> =
expert_bytes_per_layer
.iter()
.map(|_| std::collections::HashMap::new())
.collect();
let mut used: u64 = 0;
for (_, l, e) in candidates {
let size = expert_bytes_per_layer[l][e] as u64;
if size == 0 || used + size > vram_budget_bytes {
continue;
}
used += size;
layer_overrides[l].insert(e, ExpertPlacement::GpuDevice(0));
}
ResidencyPlan {
layer_plans: layer_overrides
.into_iter()
.map(|overrides| PlacementPlan {
default_placement: ExpertPlacement::Cpu,
overrides,
})
.collect(),
device_bytes_planned: used,
vram_budget_bytes,
}
}
pub fn placement_for(&self, expert_id: usize) -> ExpertPlacement {
self.overrides
.get(&expert_id)
.copied()
.unwrap_or(self.default_placement)
}
}
pub struct ResidencyPlan {
layer_plans: Vec<PlacementPlan>,
pub device_bytes_planned: u64,
pub vram_budget_bytes: u64,
}
impl ResidencyPlan {
pub fn layer_plan(&self, layer: usize) -> &PlacementPlan {
&self.layer_plans[layer]
}
pub fn n_layers(&self) -> usize {
self.layer_plans.len()
}
}
pub struct ExpertWeights {
pub gate: WeightMatrix,
pub up: WeightMatrix,
pub down: WeightMatrix,
}
#[derive(Debug, Clone, Default)]
pub struct ExpertBias {
pub gate: Vec<f32>,
pub up: Vec<f32>,
pub down: Vec<f32>,
}
pub const SWIGLU_OAI_ALPHA: f32 = 1.702;
pub const SWIGLU_OAI_LIMIT: f32 = 7.0;
pub fn swiglu_oai(gate: &[f32], up: &[f32], alpha: f32, limit: f32) -> Vec<f32> {
debug_assert_eq!(gate.len(), up.len());
gate.iter()
.zip(up.iter())
.map(|(&g, &u)| {
let x = g.min(limit);
let y = u.clamp(-limit, limit);
let out_glu = x / (1.0 + (alpha * -x).exp());
out_glu * (y + 1.0)
})
.collect()
}
pub fn route_top_k_softmax_weight(logits: &[f32], k: usize) -> RoutingDecision {
let mut idx: Vec<usize> = (0..logits.len()).collect();
idx.sort_unstable_by(|&a, &b| logits[b].partial_cmp(&logits[a]).unwrap());
let top = &idx[..k.min(idx.len())];
let selected: Vec<f32> = top.iter().map(|&i| logits[i]).collect();
let max = selected.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let exps: Vec<f32> = selected.iter().map(|&l| (l - max).exp()).collect();
let sum: f32 = exps.iter().sum();
let weights = if sum > 0.0 {
exps.iter().map(|&e| e / sum).collect()
} else {
exps
};
RoutingDecision {
expert_ids: top.to_vec(),
weights,
}
}
pub fn run_expert_oai(
hidden: &[f32],
expert: &ExpertWeights,
bias: &ExpertBias,
alpha: f32,
limit: f32,
) -> Vec<f32> {
let mut gate = expert.gate.apply(hidden);
let mut up = expert.up.apply(hidden);
for (x, b) in gate.iter_mut().zip(bias.gate.iter()) {
*x += b;
}
for (x, b) in up.iter_mut().zip(bias.up.iter()) {
*x += b;
}
let activated = swiglu_oai(&gate, &up, alpha, limit);
let mut out = expert.down.apply(&activated);
for (x, b) in out.iter_mut().zip(bias.down.iter()) {
*x += b;
}
out
}
pub fn run_expert(hidden: &[f32], expert: &ExpertWeights) -> Vec<f32> {
#[cfg(any(feature = "cuda", feature = "metal"))]
{
if let Some(out) = ferrox_core::WeightMatrix::apply_gpu_dense_ffn_swiglu(
&expert.gate,
&expert.up,
&expert.down,
hidden,
) {
return out;
}
}
#[cfg(any(feature = "cuda", feature = "metal"))]
{
if let Some(mut outs) =
ferrox_core::WeightMatrix::apply_gpu_multi(&[&expert.gate, &expert.up], hidden)
{
let up = outs.pop().unwrap();
let gate = outs.pop().unwrap();
let activated = swiglu(&gate, &up);
return expert.down.apply(&activated);
}
}
if ferrox_core::weight_matrix::cpu_int_dot_enabled() && hidden.len().is_multiple_of(32) {
let act = ferrox_quant::quantize_activations_q8(hidden);
let (g, u) = rayon::join(
|| expert.gate.apply_cpu_q8(&act),
|| expert.up.apply_cpu_q8(&act),
);
if let (Some(gate), Some(up)) = (g, u) {
let activated = swiglu(&gate, &up);
return expert.down.apply(&activated);
}
}
let (gate, up) = rayon::join(|| expert.gate.apply(hidden), || expert.up.apply(hidden));
let activated = swiglu(&gate, &up);
expert.down.apply(&activated)
}
#[cfg(any(feature = "cuda", feature = "metal"))]
pub fn run_expert_placed(
hidden: &[f32],
expert: &ExpertWeights,
placement: ExpertPlacement,
) -> Vec<f32> {
if matches!(placement, ExpertPlacement::GpuDevice(_)) {
#[cfg(any(feature = "cuda", feature = "metal"))]
{
if let Some(out) = ferrox_core::WeightMatrix::apply_gpu_dense_ffn_swiglu(
&expert.gate,
&expert.up,
&expert.down,
hidden,
) {
return out;
}
}
#[cfg(any(feature = "cuda", feature = "metal"))]
{
if let Some(mut outs) =
ferrox_core::WeightMatrix::apply_gpu_multi(&[&expert.gate, &expert.up], hidden)
{
let up = outs.pop().unwrap();
let gate = outs.pop().unwrap();
let activated = swiglu(&gate, &up);
if let Some(down) = expert.down.apply_gpu(&activated) {
return down;
}
return expert.down.apply(&activated);
}
}
if let Some(gate) = expert.gate.apply_gpu(hidden) {
if let Some(up) = expert.up.apply_gpu(hidden) {
let activated = swiglu(&gate, &up);
if let Some(down) = expert.down.apply_gpu(&activated) {
return down;
}
}
}
}
run_expert(hidden, expert)
}
#[cfg(not(any(feature = "cuda", feature = "metal")))]
pub fn run_expert_placed(
hidden: &[f32],
expert: &ExpertWeights,
_placement: ExpertPlacement,
) -> Vec<f32> {
run_expert(hidden, expert)
}
pub fn combine_expert_outputs(
routed_outputs: &[(Vec<f32>, f32)],
shared_outputs: &[Vec<f32>],
hidden_dim: usize,
) -> Vec<f32> {
let mut out = vec![0f32; hidden_dim];
for (expert_out, weight) in routed_outputs {
for (o, e) in out.iter_mut().zip(expert_out.iter()) {
*o += e * weight;
}
}
for shared_out in shared_outputs {
for (o, e) in out.iter_mut().zip(shared_out.iter()) {
*o += e;
}
}
out
}
#[cfg(test)]
mod tests {
#[test]
fn global_budget_cannot_be_multiplied_across_layers() {
let n_layers = 10;
let sizes: Vec<Vec<usize>> = (0..n_layers).map(|_| vec![100usize; 4]).collect();
let plan = PlacementPlan::plan_layers_against_global_budget(&sizes, None, 250);
let total_placed: usize = (0..n_layers)
.map(|l| {
(0..4)
.filter(|&e| plan.layer_plan(l).placement_for(e) != ExpertPlacement::Cpu)
.count()
})
.sum();
assert_eq!(
total_placed, 2,
"250 bytes fits exactly 2 x 100-byte experts, globally"
);
assert_eq!(plan.device_bytes_planned, 200);
assert!(plan.device_bytes_planned <= plan.vram_budget_bytes);
let per_layer_total: usize = (0..n_layers)
.map(|_| {
let p = PlacementPlan::from_budget(&[100; 4], None, 250);
(0..4)
.filter(|&e| p.placement_for(e) != ExpertPlacement::Cpu)
.count()
})
.sum();
assert_eq!(per_layer_total, 20, "per-layer planning overcommits 10x");
}
#[test]
fn global_planning_prioritizes_hotness_across_layers() {
let sizes: Vec<Vec<usize>> = (0..3).map(|_| vec![100usize; 2]).collect();
let mut counts: Vec<Vec<u64>> = (0..3).map(|_| vec![0u64; 2]).collect();
counts[2][1] = 50; counts[0][0] = 10;
let plan = PlacementPlan::plan_layers_against_global_budget(&sizes, Some(&counts), 200);
assert_eq!(
plan.layer_plan(2).placement_for(1),
ExpertPlacement::GpuDevice(0),
"hottest expert (layer 2) must win a slot"
);
assert_eq!(
plan.layer_plan(0).placement_for(0),
ExpertPlacement::GpuDevice(0),
"second-hottest expert (layer 0) takes the remaining slot"
);
assert_eq!(plan.device_bytes_planned, 200);
}
#[test]
fn global_planning_handles_zero_budget_and_dense_layers() {
let sizes = vec![Vec::new(), vec![100usize; 3], Vec::new()];
let plan = PlacementPlan::plan_layers_against_global_budget(&sizes, None, 0);
assert_eq!(plan.device_bytes_planned, 0);
assert_eq!(plan.n_layers(), 3);
for e in 0..3 {
assert_eq!(plan.layer_plan(1).placement_for(e), ExpertPlacement::Cpu);
}
}
use super::*;
#[test]
fn top_k_selects_highest_scoring_experts() {
let logits = vec![0.1, 5.0, 0.2, 3.0, -1.0];
let decision = route_top_k(&logits, 2, GatingFunction::Softmax, true);
assert_eq!(decision.expert_ids, vec![1, 3]);
let sum: f32 = decision.weights.iter().sum();
assert!((sum - 1.0).abs() < 1e-5);
assert!(decision.weights[0] > decision.weights[1]);
}
#[test]
fn top_k_weights_always_sum_to_one_regardless_of_k() {
let logits = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
for k in 1..=8 {
let decision = route_top_k(&logits, k, GatingFunction::Softmax, true);
let sum: f32 = decision.weights.iter().sum();
assert!((sum - 1.0).abs() < 1e-5, "k={k} sum={sum}");
}
}
#[test]
fn norm_topk_prob_false_uses_raw_full_softmax_probability_not_renormalized() {
let logits = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let decision = route_top_k(&logits, 3, GatingFunction::Softmax, false);
assert_eq!(decision.expert_ids, vec![7, 6, 5]);
let expected = [0.6323223_f32, 0.2326232, 0.0855683];
for (got, want) in decision.weights.iter().zip(expected.iter()) {
assert!((got - want).abs() < 1e-4, "got={got} want={want}");
}
let sum: f32 = decision.weights.iter().sum();
assert!(
(sum - 0.9505138).abs() < 1e-4,
"raw top-3 probability mass should be < 1 (it's a subset of a full 8-way softmax), got sum={sum}"
);
let normalized = route_top_k(&logits, 3, GatingFunction::Softmax, true);
assert_eq!(normalized.expert_ids, decision.expert_ids);
for (raw, norm) in decision.weights.iter().zip(normalized.weights.iter()) {
assert!(
(raw / sum - norm).abs() < 1e-4,
"raw={raw} sum={sum} normalized={norm}"
);
}
}
#[test]
fn sigmoid_gating_selects_same_top_experts_as_softmax_for_monotonic_logits() {
let logits = vec![0.1, 5.0, 0.2, 3.0, -1.0];
let softmax_decision = route_top_k(&logits, 2, GatingFunction::Softmax, true);
let sigmoid_decision = route_top_k(&logits, 2, GatingFunction::Sigmoid, true);
assert_eq!(softmax_decision.expert_ids, sigmoid_decision.expert_ids);
}
#[test]
fn sigmoid_gating_weights_sum_to_one() {
let logits = vec![-2.0, 0.5, 3.0, 1.2, -0.3, 4.0, 0.0, -1.5];
for k in 1..=8 {
let decision = route_top_k(&logits, k, GatingFunction::Sigmoid, true);
let sum: f32 = decision.weights.iter().sum();
assert!((sum - 1.0).abs() < 1e-5, "k={k} sum={sum}");
}
}
#[test]
fn bias_only_affects_selection_not_the_final_weight_value() {
let logits = vec![0.1, 2.0];
let bias = vec![10.0, 0.0];
let decision = route_top_k_sigmoid_with_bias(&logits, &bias, 1, true, 1.0);
assert_eq!(decision.expert_ids, vec![0]);
assert!((decision.weights[0] - sigmoid(0.1)).abs() < 1e-5);
}
#[test]
fn without_bias_selection_falls_back_to_plain_sigmoid_top_k() {
let logits = vec![-2.0, 0.5, 3.0, 1.2, -0.3, 4.0, 0.0, -1.5];
let zero_bias = vec![0.0; logits.len()];
let biased = route_top_k_sigmoid_with_bias(&logits, &zero_bias, 3, true, 1.0);
let plain = route_top_k(&logits, 3, GatingFunction::Sigmoid, true);
assert_eq!(biased.expert_ids, plain.expert_ids);
for (a, b) in biased.weights.iter().zip(plain.weights.iter()) {
assert!((a - b).abs() < 1e-6);
}
}
#[test]
fn scaling_factor_multiplies_every_weight() {
let logits = vec![1.0, 2.0, 3.0];
let bias = vec![0.0; 3];
let unscaled = route_top_k_sigmoid_with_bias(&logits, &bias, 2, true, 1.0);
let scaled = route_top_k_sigmoid_with_bias(&logits, &bias, 2, true, 2.5);
for (u, s) in unscaled.weights.iter().zip(scaled.weights.iter()) {
assert!((u * 2.5 - s).abs() < 1e-5);
}
}
#[test]
fn sigmoid_and_softmax_weights_differ_for_the_same_logits() {
let logits = vec![3.0, 1.0, -2.0, 0.5];
let softmax_decision = route_top_k(&logits, 2, GatingFunction::Softmax, true);
let sigmoid_decision = route_top_k(&logits, 2, GatingFunction::Sigmoid, true);
assert!(
(softmax_decision.weights[0] - sigmoid_decision.weights[0]).abs() > 1e-3,
"softmax and sigmoid gating should generally produce different weight splits for the same logits"
);
}
#[test]
fn sqrt_softplus_matches_hand_computed_values_at_zero_and_positive_logit() {
assert!((sqrt_softplus(0.0) - 2.0_f32.ln().sqrt()).abs() < 1e-6);
assert!((sqrt_softplus(20.0) - 20.0_f32.sqrt()).abs() < 1e-3);
}
#[test]
fn grouped_routing_picks_within_each_group_then_global_top_k() {
let logits = vec![0.1, 5.0, 0.2, 4.0];
let d = route_top_k_grouped(&logits, 2, 1, 2, GatingFunction::Softmax, true);
assert_eq!(d.expert_ids.len(), 2);
assert!(d.expert_ids.contains(&1));
assert!(d.expert_ids.contains(&3));
let sum: f32 = d.weights.iter().sum();
assert!((sum - 1.0).abs() < 1e-4);
}
#[test]
fn sqrtsoftplus_gating_selects_same_top_experts_as_softmax_for_monotonic_logits() {
let logits = vec![0.1, 5.0, 0.2, 3.0, -1.0];
let softmax_decision = route_top_k(&logits, 2, GatingFunction::Softmax, true);
let sqrtsoftplus_decision = route_top_k(&logits, 2, GatingFunction::SqrtSoftplus, true);
assert_eq!(
softmax_decision.expert_ids,
sqrtsoftplus_decision.expert_ids
);
}
#[test]
fn sqrtsoftplus_weights_sum_to_one_when_normalized() {
let logits = vec![-2.0, 0.5, 3.0, 1.2, -0.3, 4.0, 0.0, -1.5];
for k in 1..=8 {
let decision = route_top_k(&logits, k, GatingFunction::SqrtSoftplus, true);
let sum: f32 = decision.weights.iter().sum();
assert!((sum - 1.0).abs() < 1e-4, "k={k} sum={sum}");
}
}
#[test]
fn sqrtsoftplus_bias_only_affects_selection_not_the_final_weight_value() {
let logits = vec![0.1, 2.0];
let bias = vec![10.0, 0.0];
let decision = route_top_k_sqrtsoftplus_with_bias(&logits, &bias, 1, true, 1.0);
assert_eq!(decision.expert_ids, vec![0]);
assert!((decision.weights[0] - sqrt_softplus(0.1)).abs() < 1e-5);
}
#[test]
fn sqrtsoftplus_without_bias_selection_falls_back_to_plain_top_k() {
let logits = vec![-2.0, 0.5, 3.0, 1.2, -0.3, 4.0, 0.0, -1.5];
let zero_bias = vec![0.0; logits.len()];
let biased = route_top_k_sqrtsoftplus_with_bias(&logits, &zero_bias, 3, true, 1.0);
let plain = route_top_k(&logits, 3, GatingFunction::SqrtSoftplus, true);
assert_eq!(biased.expert_ids, plain.expert_ids);
for (a, b) in biased.weights.iter().zip(plain.weights.iter()) {
assert!((a - b).abs() < 1e-6);
}
}
#[test]
fn hash_routing_uses_the_fixed_table_ids_regardless_of_logit_ranking() {
let logits = vec![100.0, 1.0, 0.5, -3.0];
let hash_expert_ids = vec![2usize, 1usize];
let decision = route_hash(&hash_expert_ids, &logits, true, 1.0);
assert_eq!(decision.expert_ids, vec![2, 1]);
}
#[test]
fn hash_routing_weights_come_from_the_real_router_logits_not_a_fixed_split() {
let logits = vec![-5.0, 0.1, 3.0, -5.0];
let hash_expert_ids = vec![2usize, 1usize];
let decision = route_hash(&hash_expert_ids, &logits, true, 1.0);
assert!(decision.weights[0] > decision.weights[1]);
let sum: f32 = decision.weights.iter().sum();
assert!((sum - 1.0).abs() < 1e-5);
let expected0 = sqrt_softplus(3.0) / (sqrt_softplus(3.0) + sqrt_softplus(0.1));
assert!((decision.weights[0] - expected0).abs() < 1e-5);
}
#[test]
fn hash_routing_scaling_factor_multiplies_every_weight() {
let logits = vec![1.0, 2.0, 3.0];
let hash_expert_ids = vec![0usize, 2usize];
let unscaled = route_hash(&hash_expert_ids, &logits, true, 1.0);
let scaled = route_hash(&hash_expert_ids, &logits, true, 2.5);
for (u, s) in unscaled.weights.iter().zip(scaled.weights.iter()) {
assert!((u * 2.5 - s).abs() < 1e-5);
}
}
#[test]
fn placement_plan_defaults_to_cpu_for_unlisted_experts() {
let plan = PlacementPlan::hot_experts_on_gpu(256, 8);
assert_eq!(plan.placement_for(0), ExpertPlacement::GpuDevice(0));
assert_eq!(plan.placement_for(7), ExpertPlacement::GpuDevice(0));
assert_eq!(plan.placement_for(8), ExpertPlacement::Cpu);
assert_eq!(plan.placement_for(255), ExpertPlacement::Cpu);
}
#[test]
fn all_cpu_plan_never_returns_gpu() {
let plan = PlacementPlan::all_cpu(64);
for i in 0..64 {
assert_eq!(plan.placement_for(i), ExpertPlacement::Cpu);
}
}
#[test]
fn from_budget_fits_as_many_experts_as_the_vram_budget_allows() {
let sizes = vec![100usize, 100, 100, 100];
let plan = PlacementPlan::from_budget(&sizes, None, 250);
let on_gpu = (0..4)
.filter(|&i| plan.placement_for(i) == ExpertPlacement::GpuDevice(0))
.count();
assert_eq!(on_gpu, 2);
}
#[test]
fn from_budget_prioritizes_the_most_frequently_activated_experts() {
let sizes = vec![50usize, 50, 50, 50];
let counts = vec![1u64, 2, 100, 3];
let plan = PlacementPlan::from_budget(&sizes, Some(&counts), 50);
assert_eq!(
plan.placement_for(2),
ExpertPlacement::GpuDevice(0),
"the hottest expert (index 2) must be the one placed on GPU"
);
assert_eq!(plan.placement_for(0), ExpertPlacement::Cpu);
assert_eq!(plan.placement_for(1), ExpertPlacement::Cpu);
assert_eq!(plan.placement_for(3), ExpertPlacement::Cpu);
}
#[test]
fn from_budget_skips_an_expert_that_does_not_fit_and_tries_the_next() {
let sizes = vec![200usize, 60, 60];
let plan = PlacementPlan::from_budget(&sizes, None, 120);
assert_eq!(plan.placement_for(0), ExpertPlacement::Cpu);
assert_eq!(plan.placement_for(1), ExpertPlacement::GpuDevice(0));
assert_eq!(plan.placement_for(2), ExpertPlacement::GpuDevice(0));
}
#[test]
fn from_budget_with_zero_vram_places_nothing_on_gpu() {
let sizes = vec![10usize, 20, 30];
let plan = PlacementPlan::from_budget(&sizes, None, 0);
for i in 0..3 {
assert_eq!(plan.placement_for(i), ExpertPlacement::Cpu);
}
}
#[test]
fn from_budget_ignores_mismatched_activation_counts_length_rather_than_panicking() {
let sizes = vec![10usize, 10];
let counts = vec![1u64]; let plan = PlacementPlan::from_budget(&sizes, Some(&counts), 100);
assert_eq!(plan.placement_for(0), ExpertPlacement::GpuDevice(0));
assert_eq!(plan.placement_for(1), ExpertPlacement::GpuDevice(0));
}
#[test]
fn combine_expert_outputs_weights_routed_and_adds_shared() {
let routed = vec![(vec![2.0, 2.0], 0.5), (vec![4.0, 4.0], 0.5)];
let shared = vec![vec![1.0, 1.0]];
let out = combine_expert_outputs(&routed, &shared, 2);
assert_eq!(out, vec![4.0, 4.0]);
}
#[test]
fn run_expert_produces_correct_output_dimension() {
use ferrox_core::tensor::Tensor;
let hidden_dim = 4;
let ffn_dim = 3;
let expert = ExpertWeights {
gate: WeightMatrix::F32(Tensor::new(
vec![0.1; ffn_dim * hidden_dim],
vec![ffn_dim, hidden_dim],
)),
up: WeightMatrix::F32(Tensor::new(
vec![0.2; ffn_dim * hidden_dim],
vec![ffn_dim, hidden_dim],
)),
down: WeightMatrix::F32(Tensor::new(
vec![0.3; hidden_dim * ffn_dim],
vec![hidden_dim, ffn_dim],
)),
};
let hidden = vec![1.0, -1.0, 0.5, 0.5];
let out = run_expert(&hidden, &expert);
assert_eq!(out.len(), hidden_dim);
assert!(out.iter().all(|v| v.is_finite()));
}
#[test]
fn run_expert_placed_matches_run_expert_when_nothing_is_gpu_dispatched() {
use ferrox_core::tensor::Tensor;
let hidden_dim = 4;
let ffn_dim = 3;
let expert = ExpertWeights {
gate: WeightMatrix::F32(Tensor::new(
vec![0.1; ffn_dim * hidden_dim],
vec![ffn_dim, hidden_dim],
)),
up: WeightMatrix::F32(Tensor::new(
vec![0.2; ffn_dim * hidden_dim],
vec![ffn_dim, hidden_dim],
)),
down: WeightMatrix::F32(Tensor::new(
vec![0.3; hidden_dim * ffn_dim],
vec![hidden_dim, ffn_dim],
)),
};
let hidden = vec![1.0, -1.0, 0.5, 0.5];
let expected = run_expert(&hidden, &expert);
assert_eq!(
run_expert_placed(&hidden, &expert, ExpertPlacement::Cpu),
expected
);
assert_eq!(
run_expert_placed(&hidden, &expert, ExpertPlacement::GpuDevice(0)),
expected,
"F32 has no GPU kernel, so GpuDevice placement must still fall through to the CPU path"
);
}
#[cfg(any(feature = "cuda", feature = "metal"))]
#[test]
#[ignore = "requires real GPU hardware (CUDA or Metal) -- run with --ignored"]
fn run_expert_placed_on_gpu_matches_cpu_for_a_real_quantized_expert() {
let hidden_dim = 32;
let ffn_dim = 32; let make_row = |cols: usize, seed: f32| -> Vec<f32> {
(0..cols)
.map(|i| ((i as f32) - (cols as f32) / 2.0) * 0.01 * seed)
.collect()
};
let quantize_matrix = |rows: usize, cols: usize, seed: f32| {
let mut packed = Vec::new();
for r in 0..rows {
packed.extend(ferrox_quant::quantize_q8_0(&make_row(
cols,
seed + r as f32,
)));
}
WeightMatrix::Quantized {
data: ferrox_core::weight_matrix::WeightBytes::Owned(packed),
rows,
cols,
kind: ferrox_core::weight_matrix::QuantKind::Q8_0,
}
};
let expert = ExpertWeights {
gate: quantize_matrix(ffn_dim, hidden_dim, 1.0),
up: quantize_matrix(ffn_dim, hidden_dim, 2.0),
down: quantize_matrix(hidden_dim, ffn_dim, 3.0),
};
let hidden = make_row(hidden_dim, 0.5);
let cpu = run_expert_placed(&hidden, &expert, ExpertPlacement::Cpu);
let gpu = run_expert_placed(&hidden, &expert, ExpertPlacement::GpuDevice(0));
assert_eq!(cpu.len(), gpu.len());
for (c, g) in cpu.iter().zip(gpu.iter()) {
assert!((c - g).abs() < 1e-1, "cpu={c} gpu={g}");
}
}
}