use super::{NodeResources, Workload};
pub trait SchedulingAlgorithm: Send + Sync {
fn score(&self, workload: &Workload, node: &NodeResources) -> f64;
fn name(&self) -> &str;
}
#[derive(Debug, Clone)]
pub struct BinPackScheduler {
cpu_weight: f64,
memory_weight: f64,
gpu_weight: f64,
}
impl BinPackScheduler {
pub fn new() -> Self {
Self {
cpu_weight: 1.0,
memory_weight: 1.0,
gpu_weight: 2.0, }
}
pub fn with_weights(mut self, cpu: f64, memory: f64, gpu: f64) -> Self {
self.cpu_weight = cpu;
self.memory_weight = memory;
self.gpu_weight = gpu;
self
}
}
impl Default for BinPackScheduler {
fn default() -> Self {
Self::new()
}
}
impl SchedulingAlgorithm for BinPackScheduler {
fn score(&self, workload: &Workload, node: &NodeResources) -> f64 {
let cpu_util = node.cpu_allocated as f64 / node.cpu_capacity as f64;
let mem_util = node.memory_allocated as f64 / node.memory_capacity as f64;
let gpu_util = if !node.gpus.is_empty() {
node.gpus_allocated.len() as f64 / node.gpus.len() as f64
} else {
0.0
};
let base_score = (cpu_util * self.cpu_weight + mem_util * self.memory_weight + gpu_util * self.gpu_weight)
/ (self.cpu_weight + self.memory_weight + self.gpu_weight);
let cpu_headroom = (node.cpu_available() as f64 - workload.resources.cpu_millis as f64)
/ node.cpu_capacity as f64;
let mem_headroom = (node.memory_available() as f64 - workload.resources.memory_mb as f64)
/ node.memory_capacity as f64;
let headroom_penalty = if cpu_headroom < 0.05 || mem_headroom < 0.05 {
0.1
} else {
0.0
};
(base_score - headroom_penalty).max(0.0)
}
fn name(&self) -> &str {
"bin-pack"
}
}
#[derive(Debug, Clone)]
pub struct SpreadScheduler {
topology_key: Option<String>,
}
impl SpreadScheduler {
pub fn new() -> Self {
Self { topology_key: None }
}
pub fn with_topology(mut self, key: impl Into<String>) -> Self {
self.topology_key = Some(key.into());
self
}
}
impl Default for SpreadScheduler {
fn default() -> Self {
Self::new()
}
}
impl SchedulingAlgorithm for SpreadScheduler {
fn score(&self, _workload: &Workload, node: &NodeResources) -> f64 {
let cpu_util = node.cpu_allocated as f64 / node.cpu_capacity as f64;
let mem_util = node.memory_allocated as f64 / node.memory_capacity as f64;
let spread_score = 1.0 - (cpu_util + mem_util) / 2.0;
let topology_bonus = if let Some(key) = &self.topology_key {
if node.labels.contains_key(key) { 0.1 } else { 0.0 }
} else {
0.0
};
spread_score + topology_bonus
}
fn name(&self) -> &str {
"spread"
}
}
#[derive(Debug, Clone)]
pub struct GpuLocalityScheduler {
prefer_tensor_cores: bool,
min_compute_capability: Option<f32>,
prefer_nvlink: bool,
}
impl GpuLocalityScheduler {
pub fn new() -> Self {
Self {
prefer_tensor_cores: true,
min_compute_capability: None,
prefer_nvlink: true,
}
}
pub fn min_compute_capability(mut self, cc: f32) -> Self {
self.min_compute_capability = Some(cc);
self
}
pub fn prefer_tensor_cores(mut self, prefer: bool) -> Self {
self.prefer_tensor_cores = prefer;
self
}
}
impl Default for GpuLocalityScheduler {
fn default() -> Self {
Self::new()
}
}
impl SchedulingAlgorithm for GpuLocalityScheduler {
fn score(&self, workload: &Workload, node: &NodeResources) -> f64 {
if workload.resources.gpu_count == 0 {
let cpu_util = node.cpu_allocated as f64 / node.cpu_capacity as f64;
return 1.0 - cpu_util;
}
let available_gpus: Vec<_> = node.gpus.iter()
.filter(|g| !node.gpus_allocated.contains(&g.device_id))
.collect();
if available_gpus.is_empty() {
return 0.0;
}
let mut score = 0.5;
let total_available_mem: u64 = available_gpus.iter()
.map(|g| g.available_memory_mb())
.sum();
let required_mem = workload.resources.gpu_memory_mb * workload.resources.gpu_count as u64;
if total_available_mem >= required_mem {
score += 0.2;
}
if self.prefer_tensor_cores {
let tensor_core_count = available_gpus.iter()
.filter(|g| g.tensor_cores)
.count();
score += 0.1 * (tensor_core_count as f64 / available_gpus.len() as f64);
}
if let Some(min_cc) = self.min_compute_capability {
let meets_cc = available_gpus.iter()
.all(|g| g.compute_capability.map(|cc| cc >= min_cc).unwrap_or(false));
if meets_cc {
score += 0.1;
} else {
score -= 0.2;
}
}
let gpu_ids: Vec<_> = available_gpus.iter().map(|g| g.device_id).collect();
if gpu_ids.len() >= workload.resources.gpu_count as usize {
let contiguous = gpu_ids.windows(2)
.all(|w| w[1] == w[0] + 1);
if contiguous {
score += 0.1;
}
}
score.min(1.0)
}
fn name(&self) -> &str {
"gpu-locality"
}
}
#[derive(Debug)]
pub struct LearnedScheduler {
weights: parking_lot::RwLock<Vec<f64>>,
learning_rate: f64,
num_features: usize,
history: parking_lot::RwLock<Vec<SchedulingFeedback>>,
}
#[derive(Debug, Clone)]
pub struct SchedulingFeedback {
pub features: Vec<f64>,
pub performance: f64,
}
impl LearnedScheduler {
pub fn new() -> Self {
let num_features = 8; Self {
weights: parking_lot::RwLock::new(vec![0.5; num_features]),
learning_rate: 0.01,
num_features,
history: parking_lot::RwLock::new(Vec::new()),
}
}
fn extract_features(&self, workload: &Workload, node: &NodeResources) -> Vec<f64> {
vec![
node.cpu_allocated as f64 / node.cpu_capacity as f64,
node.memory_allocated as f64 / node.memory_capacity as f64,
if node.gpus.is_empty() { 0.0 } else { node.gpus_allocated.len() as f64 / node.gpus.len() as f64 },
(node.cpu_available() as f64 - workload.resources.cpu_millis as f64) / node.cpu_capacity as f64,
(node.memory_available() as f64 - workload.resources.memory_mb as f64) / node.memory_capacity as f64,
workload.priority as f64 / 100.0,
if workload.resources.gpu_count > 0 { 1.0 } else { 0.0 },
if node.schedulable { 1.0 } else { 0.0 },
]
}
pub fn record_feedback(&self, feedback: SchedulingFeedback) {
let mut weights = self.weights.write();
let prediction: f64 = feedback.features.iter()
.zip(weights.iter())
.map(|(f, w)| f * w)
.sum();
let error = feedback.performance - prediction;
for (i, feature) in feedback.features.iter().enumerate() {
weights[i] += self.learning_rate * error * feature;
weights[i] = weights[i].clamp(-2.0, 2.0);
}
self.history.write().push(feedback);
}
pub fn weights(&self) -> Vec<f64> {
self.weights.read().clone()
}
}
impl Default for LearnedScheduler {
fn default() -> Self {
Self::new()
}
}
impl SchedulingAlgorithm for LearnedScheduler {
fn score(&self, workload: &Workload, node: &NodeResources) -> f64 {
let features = self.extract_features(workload, node);
let weights = self.weights.read();
let score: f64 = features.iter()
.zip(weights.iter())
.map(|(f, w)| f * w)
.sum();
1.0 / (1.0 + (-score).exp())
}
fn name(&self) -> &str {
"learned"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::NodeId;
fn test_node(cpu_alloc: u64, mem_alloc: u64) -> NodeResources {
let mut node = NodeResources::new(NodeId::new(), 4000, 8192);
node.cpu_allocated = cpu_alloc;
node.memory_allocated = mem_alloc;
node
}
#[test]
fn test_bin_pack_prefers_utilized() {
let scheduler = BinPackScheduler::new();
let workload = Workload::new("w1", "test");
let low_util = test_node(1000, 2048);
let high_util = test_node(3000, 6144);
let low_score = scheduler.score(&workload, &low_util);
let high_score = scheduler.score(&workload, &high_util);
assert!(high_score > low_score, "Bin-pack should prefer higher utilization");
}
#[test]
fn test_spread_prefers_empty() {
let scheduler = SpreadScheduler::new();
let workload = Workload::new("w1", "test");
let low_util = test_node(1000, 2048);
let high_util = test_node(3000, 6144);
let low_score = scheduler.score(&workload, &low_util);
let high_score = scheduler.score(&workload, &high_util);
assert!(low_score > high_score, "Spread should prefer lower utilization");
}
#[test]
fn test_learned_scheduler() {
let scheduler = LearnedScheduler::new();
let workload = Workload::new("w1", "test");
let node = test_node(2000, 4096);
let score = scheduler.score(&workload, &node);
assert!(score >= 0.0 && score <= 1.0, "Score should be bounded");
let features = vec![0.5, 0.5, 0.0, 0.25, 0.25, 0.0, 0.0, 1.0];
scheduler.record_feedback(SchedulingFeedback {
features,
performance: 0.9,
});
let weights = scheduler.weights();
assert!(weights.iter().any(|w| *w != 0.5));
}
}