pub mod algorithms;
pub mod deadline;
pub mod gang;
pub mod optimized;
pub mod placement;
pub mod preemption;
pub mod queue;
pub mod reconcile;
pub mod sim;
use std::collections::HashMap;
use std::sync::Arc;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use tracing::{debug, info, warn};
use crate::types::{NodeId, GpuResources};
pub use algorithms::{SchedulingAlgorithm, BinPackScheduler, SpreadScheduler, GpuLocalityScheduler, LearnedScheduler, SchedulingFeedback};
pub use deadline::{DeadlineEntry, DeadlineQueue, Eligibility, MissPolicy, TickDeadlineScheduler, TickOutcome};
pub use gang::{GangDecision, GangScheduler};
pub use reconcile::{Assignment, MetricsSource, ReconcileReport, Reconciler, TaskStatus};
pub use optimized::{OptimizedScheduler, WorkloadBatch, SchedulerStats, ClusterUtilization, FFDBinPacker};
pub use placement::{PlacementConstraint, AffinityRule, Affinity};
pub use preemption::{PreemptionPolicy, PriorityClass};
pub use queue::{SchedulingQueue, QueuedWorkload};
pub use sim::{AgentPolicy, CoPlacement, GangGroup, MemberRole, Region3D, SimCell, SimMember, SimWorld};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceRequirements {
pub cpu_millis: u64,
pub memory_mb: u64,
pub gpu_count: u32,
pub gpu_memory_mb: u64,
pub storage_mb: u64,
pub network_mbps: u32,
}
impl Default for ResourceRequirements {
fn default() -> Self {
Self {
cpu_millis: 100,
memory_mb: 128,
gpu_count: 0,
gpu_memory_mb: 0,
storage_mb: 0,
network_mbps: 0,
}
}
}
impl ResourceRequirements {
pub fn new() -> Self {
Self::default()
}
pub fn cpu(mut self, millis: u64) -> Self {
self.cpu_millis = millis;
self
}
pub fn memory(mut self, mb: u64) -> Self {
self.memory_mb = mb;
self
}
pub fn gpu(mut self, count: u32, memory_mb: u64) -> Self {
self.gpu_count = count;
self.gpu_memory_mb = memory_mb;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeResources {
pub node_id: NodeId,
pub cpu_capacity: u64,
pub cpu_allocated: u64,
pub memory_capacity: u64,
pub memory_allocated: u64,
pub gpus: Vec<GpuResources>,
pub gpus_allocated: Vec<u32>,
pub labels: HashMap<String, String>,
pub taints: Vec<Taint>,
pub schedulable: bool,
pub conditions: Vec<NodeCondition>,
}
impl NodeResources {
pub fn new(node_id: NodeId, cpu_capacity: u64, memory_capacity: u64) -> Self {
Self {
node_id,
cpu_capacity,
cpu_allocated: 0,
memory_capacity,
memory_allocated: 0,
gpus: Vec::new(),
gpus_allocated: Vec::new(),
labels: HashMap::new(),
taints: Vec::new(),
schedulable: true,
conditions: Vec::new(),
}
}
pub fn with_gpu(mut self, gpu: GpuResources) -> Self {
self.gpus.push(gpu);
self
}
pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.labels.insert(key.into(), value.into());
self
}
pub fn with_taint(mut self, taint: Taint) -> Self {
self.taints.push(taint);
self
}
pub fn cpu_available(&self) -> u64 {
self.cpu_capacity.saturating_sub(self.cpu_allocated)
}
pub fn memory_available(&self) -> u64 {
self.memory_capacity.saturating_sub(self.memory_allocated)
}
pub fn gpus_available(&self) -> usize {
self.gpus.len() - self.gpus_allocated.len()
}
pub fn can_fit(&self, req: &ResourceRequirements) -> bool {
if !self.schedulable {
return false;
}
if self.cpu_available() < req.cpu_millis {
return false;
}
if self.memory_available() < req.memory_mb {
return false;
}
if req.gpu_count > 0 {
let available_gpus: Vec<_> = self.gpus.iter()
.filter(|g| !self.gpus_allocated.contains(&g.device_id))
.filter(|g| g.available_memory_mb() >= req.gpu_memory_mb)
.collect();
if available_gpus.len() < req.gpu_count as usize {
return false;
}
}
true
}
pub fn allocate(&mut self, req: &ResourceRequirements) -> bool {
if !self.can_fit(req) {
return false;
}
self.cpu_allocated += req.cpu_millis;
self.memory_allocated += req.memory_mb;
for _ in 0..req.gpu_count {
if let Some(gpu) = self.gpus.iter()
.find(|g| !self.gpus_allocated.contains(&g.device_id)
&& g.available_memory_mb() >= req.gpu_memory_mb)
{
self.gpus_allocated.push(gpu.device_id);
}
}
true
}
pub fn release(&mut self, req: &ResourceRequirements, gpu_ids: &[u32]) {
self.cpu_allocated = self.cpu_allocated.saturating_sub(req.cpu_millis);
self.memory_allocated = self.memory_allocated.saturating_sub(req.memory_mb);
self.gpus_allocated.retain(|id| !gpu_ids.contains(id));
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Taint {
pub key: String,
pub value: String,
pub effect: TaintEffect,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TaintEffect {
NoSchedule,
PreferNoSchedule,
NoExecute,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeCondition {
pub condition_type: String,
pub status: bool,
pub last_transition: chrono::DateTime<chrono::Utc>,
pub reason: Option<String>,
pub message: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Workload {
pub id: String,
pub name: String,
pub namespace: String,
pub resources: ResourceRequirements,
pub priority: i32,
pub priority_class: Option<String>,
pub constraints: Vec<PlacementConstraint>,
pub affinity: Option<Affinity>,
pub tolerations: Vec<Toleration>,
pub preemption_policy: PreemptionPolicy,
pub created_at: chrono::DateTime<chrono::Utc>,
}
impl Workload {
pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
namespace: "default".to_string(),
resources: ResourceRequirements::default(),
priority: 0,
priority_class: None,
constraints: Vec::new(),
affinity: None,
tolerations: Vec::new(),
preemption_policy: PreemptionPolicy::PreemptLowerPriority,
created_at: chrono::Utc::now(),
}
}
pub fn with_resources(mut self, resources: ResourceRequirements) -> Self {
self.resources = resources;
self
}
pub fn with_priority(mut self, priority: i32) -> Self {
self.priority = priority;
self
}
pub fn with_constraint(mut self, constraint: PlacementConstraint) -> Self {
self.constraints.push(constraint);
self
}
pub fn with_affinity(mut self, affinity: Affinity) -> Self {
self.affinity = Some(affinity);
self
}
pub fn with_toleration(mut self, toleration: Toleration) -> Self {
self.tolerations.push(toleration);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Toleration {
pub key: Option<String>,
pub operator: TolerationOperator,
pub value: Option<String>,
pub effect: Option<TaintEffect>,
pub toleration_seconds: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TolerationOperator {
Equal,
Exists,
}
#[derive(Debug, Clone)]
pub struct SchedulingDecision {
pub workload_id: String,
pub node_id: Option<NodeId>,
pub score: f64,
pub reason: String,
pub preempted: Vec<String>,
pub latency_ms: u64,
}
pub struct Scheduler {
nodes: Arc<RwLock<HashMap<NodeId, NodeResources>>>,
queue: Arc<SchedulingQueue>,
algorithm: Arc<dyn SchedulingAlgorithm + Send + Sync>,
assignments: Arc<RwLock<HashMap<String, NodeId>>>,
priority_classes: Arc<RwLock<HashMap<String, PriorityClass>>>,
}
impl Scheduler {
pub fn new() -> Self {
Self {
nodes: Arc::new(RwLock::new(HashMap::new())),
queue: Arc::new(SchedulingQueue::new()),
algorithm: Arc::new(BinPackScheduler::new()),
assignments: Arc::new(RwLock::new(HashMap::new())),
priority_classes: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn with_algorithm<A: SchedulingAlgorithm + Send + Sync + 'static>(algorithm: A) -> Self {
Self {
nodes: Arc::new(RwLock::new(HashMap::new())),
queue: Arc::new(SchedulingQueue::new()),
algorithm: Arc::new(algorithm),
assignments: Arc::new(RwLock::new(HashMap::new())),
priority_classes: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn register_node(&self, node: NodeResources) {
info!(node_id = %node.node_id, "Registering node");
self.nodes.write().insert(node.node_id, node);
}
pub fn unregister_node(&self, node_id: &NodeId) {
info!(node_id = %node_id, "Unregistering node");
self.nodes.write().remove(node_id);
}
pub fn update_node(&self, node: NodeResources) {
self.nodes.write().insert(node.node_id, node);
}
pub fn node_count(&self) -> usize {
self.nodes.read().len()
}
pub fn submit(&self, workload: Workload) {
debug!(workload_id = %workload.id, "Submitting workload");
self.queue.enqueue(workload);
}
pub fn schedule_next(&self) -> Option<SchedulingDecision> {
let workload = self.queue.dequeue()?;
Some(self.schedule(&workload))
}
pub fn schedule(&self, workload: &Workload) -> SchedulingDecision {
let start = std::time::Instant::now();
let nodes = self.nodes.read();
let candidates: Vec<_> = nodes.values()
.filter(|n| n.can_fit(&workload.resources))
.filter(|n| self.check_constraints(workload, n))
.filter(|n| self.check_taints(workload, n))
.collect();
if candidates.is_empty() {
drop(nodes);
if let Some(decision) = self.try_preemption(workload) {
return decision;
}
return SchedulingDecision {
workload_id: workload.id.clone(),
node_id: None,
score: 0.0,
reason: "No suitable nodes available".to_string(),
preempted: Vec::new(),
latency_ms: start.elapsed().as_millis() as u64,
};
}
let scored: Vec<_> = candidates.iter()
.map(|n| (n, self.algorithm.score(workload, n)))
.collect();
let (best_node, score) = scored.iter()
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(n, s)| (*n, *s))
.unwrap();
let node_id = best_node.node_id;
drop(nodes);
if let Some(node) = self.nodes.write().get_mut(&node_id) {
node.allocate(&workload.resources);
}
self.assignments.write().insert(workload.id.clone(), node_id);
info!(
workload_id = %workload.id,
node_id = %node_id,
score = score,
"Workload scheduled"
);
SchedulingDecision {
workload_id: workload.id.clone(),
node_id: Some(node_id),
score,
reason: "Scheduled successfully".to_string(),
preempted: Vec::new(),
latency_ms: start.elapsed().as_millis() as u64,
}
}
fn check_constraints(&self, workload: &Workload, node: &NodeResources) -> bool {
for constraint in &workload.constraints {
if !constraint.matches(node) {
return false;
}
}
if let Some(affinity) = &workload.affinity {
if !affinity.matches(node) {
return false;
}
}
true
}
fn check_taints(&self, workload: &Workload, node: &NodeResources) -> bool {
for taint in &node.taints {
let tolerated = workload.tolerations.iter().any(|t| {
let key_matches = t.key.as_ref().map(|k| k == &taint.key).unwrap_or(true);
let value_matches = match t.operator {
TolerationOperator::Exists => true,
TolerationOperator::Equal => {
t.value.as_ref().map(|v| v == &taint.value).unwrap_or(false)
}
};
let effect_matches = t.effect.map(|e| e == taint.effect).unwrap_or(true);
key_matches && value_matches && effect_matches
});
if !tolerated && taint.effect == TaintEffect::NoSchedule {
return false;
}
}
true
}
fn try_preemption(&self, workload: &Workload) -> Option<SchedulingDecision> {
if workload.preemption_policy == PreemptionPolicy::Never {
return None;
}
let assignments = self.assignments.read();
let mut nodes = self.nodes.write();
for (node_id, node) in nodes.iter_mut() {
let preemptable: Vec<_> = assignments.iter()
.filter(|(_, n)| *n == node_id)
.map(|(w, _)| w.clone())
.collect();
if node.can_fit(&workload.resources) || !preemptable.is_empty() {
warn!(
workload_id = %workload.id,
node_id = %node_id,
"Preemption would be required"
);
}
}
None
}
pub fn get_assignment(&self, workload_id: &str) -> Option<NodeId> {
self.assignments.read().get(workload_id).copied()
}
pub fn release(&self, workload_id: &str, resources: &ResourceRequirements, gpu_ids: &[u32]) {
if let Some(node_id) = self.assignments.write().remove(workload_id) {
if let Some(node) = self.nodes.write().get_mut(&node_id) {
node.release(resources, gpu_ids);
}
}
}
pub fn queue_len(&self) -> usize {
self.queue.len()
}
pub fn register_priority_class(&self, class: PriorityClass) {
self.priority_classes.write().insert(class.name.clone(), class);
}
}
impl Default for Scheduler {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_node_resources() {
let mut node = NodeResources::new(NodeId::new(), 4000, 8192);
let req = ResourceRequirements::new().cpu(1000).memory(2048);
assert!(node.can_fit(&req));
assert!(node.allocate(&req));
assert_eq!(node.cpu_available(), 3000);
assert_eq!(node.memory_available(), 6144);
}
#[test]
fn test_scheduler_basic() {
let scheduler = Scheduler::new();
let node = NodeResources::new(NodeId::new(), 4000, 8192);
scheduler.register_node(node);
let workload = Workload::new("w1", "test")
.with_resources(ResourceRequirements::new().cpu(1000).memory(1024));
let decision = scheduler.schedule(&workload);
assert!(decision.node_id.is_some());
}
#[test]
fn test_scheduler_no_capacity() {
let scheduler = Scheduler::new();
let node = NodeResources::new(NodeId::new(), 1000, 1024);
scheduler.register_node(node);
let workload = Workload::new("w1", "test")
.with_resources(ResourceRequirements::new().cpu(2000).memory(2048));
let decision = scheduler.schedule(&workload);
assert!(decision.node_id.is_none());
}
}