1pub mod algorithms;
11pub mod deadline;
12pub mod gang;
13pub mod optimized;
14pub mod placement;
15pub mod preemption;
16pub mod queue;
17pub mod reconcile;
18pub mod sim;
19
20use std::collections::HashMap;
21use std::sync::Arc;
22use parking_lot::RwLock;
23use serde::{Deserialize, Serialize};
24use tracing::{debug, info, warn};
25
26use crate::types::{NodeId, GpuResources};
27
28pub use algorithms::{SchedulingAlgorithm, BinPackScheduler, SpreadScheduler, GpuLocalityScheduler, LearnedScheduler, SchedulingFeedback};
29pub use deadline::{DeadlineEntry, DeadlineQueue, Eligibility, MissPolicy, TickDeadlineScheduler, TickOutcome};
30pub use gang::{GangDecision, GangReservation, GangScheduler};
31pub use reconcile::{Assignment, MetricsSource, ReconcileReport, Reconciler, TaskStatus};
32pub use optimized::{OptimizedScheduler, WorkloadBatch, SchedulerStats, ClusterUtilization, FFDBinPacker};
33pub use placement::{PlacementConstraint, AffinityRule, Affinity};
34pub use preemption::{PreemptionPolicy, PriorityClass};
35pub use queue::{SchedulingQueue, QueuedWorkload};
36pub use sim::{AgentPolicy, CoPlacement, GangGroup, MemberRole, Region3D, SimCell, SimMember, SimWorld};
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct ResourceRequirements {
41 pub cpu_millis: u64,
43 pub memory_mb: u64,
45 pub gpu_count: u32,
47 pub gpu_memory_mb: u64,
49 pub storage_mb: u64,
51 pub network_mbps: u32,
53}
54
55impl Default for ResourceRequirements {
56 fn default() -> Self {
57 Self {
58 cpu_millis: 100,
59 memory_mb: 128,
60 gpu_count: 0,
61 gpu_memory_mb: 0,
62 storage_mb: 0,
63 network_mbps: 0,
64 }
65 }
66}
67
68impl ResourceRequirements {
69 pub fn new() -> Self {
71 Self::default()
72 }
73
74 pub fn cpu(mut self, millis: u64) -> Self {
76 self.cpu_millis = millis;
77 self
78 }
79
80 pub fn memory(mut self, mb: u64) -> Self {
82 self.memory_mb = mb;
83 self
84 }
85
86 pub fn gpu(mut self, count: u32, memory_mb: u64) -> Self {
88 self.gpu_count = count;
89 self.gpu_memory_mb = memory_mb;
90 self
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct NodeResources {
97 pub node_id: NodeId,
99 pub cpu_capacity: u64,
101 pub cpu_allocated: u64,
103 pub memory_capacity: u64,
105 pub memory_allocated: u64,
107 pub gpus: Vec<GpuResources>,
109 pub gpus_allocated: Vec<u32>,
111 pub labels: HashMap<String, String>,
113 pub taints: Vec<Taint>,
115 pub schedulable: bool,
117 pub conditions: Vec<NodeCondition>,
119}
120
121impl NodeResources {
122 pub fn new(node_id: NodeId, cpu_capacity: u64, memory_capacity: u64) -> Self {
124 Self {
125 node_id,
126 cpu_capacity,
127 cpu_allocated: 0,
128 memory_capacity,
129 memory_allocated: 0,
130 gpus: Vec::new(),
131 gpus_allocated: Vec::new(),
132 labels: HashMap::new(),
133 taints: Vec::new(),
134 schedulable: true,
135 conditions: Vec::new(),
136 }
137 }
138
139 pub fn with_gpu(mut self, gpu: GpuResources) -> Self {
141 self.gpus.push(gpu);
142 self
143 }
144
145 pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
147 self.labels.insert(key.into(), value.into());
148 self
149 }
150
151 pub fn with_taint(mut self, taint: Taint) -> Self {
153 self.taints.push(taint);
154 self
155 }
156
157 pub fn cpu_available(&self) -> u64 {
159 self.cpu_capacity.saturating_sub(self.cpu_allocated)
160 }
161
162 pub fn memory_available(&self) -> u64 {
164 self.memory_capacity.saturating_sub(self.memory_allocated)
165 }
166
167 pub fn gpus_available(&self) -> usize {
169 self.gpus.len() - self.gpus_allocated.len()
170 }
171
172 pub fn can_fit(&self, req: &ResourceRequirements) -> bool {
174 if !self.schedulable {
175 return false;
176 }
177
178 if self.cpu_available() < req.cpu_millis {
179 return false;
180 }
181
182 if self.memory_available() < req.memory_mb {
183 return false;
184 }
185
186 if req.gpu_count > 0 {
187 let available_gpus: Vec<_> = self.gpus.iter()
188 .filter(|g| !self.gpus_allocated.contains(&g.device_id))
189 .filter(|g| g.available_memory_mb() >= req.gpu_memory_mb)
190 .collect();
191
192 if available_gpus.len() < req.gpu_count as usize {
193 return false;
194 }
195 }
196
197 true
198 }
199
200 pub fn allocate(&mut self, req: &ResourceRequirements) -> bool {
202 if !self.can_fit(req) {
203 return false;
204 }
205
206 self.cpu_allocated += req.cpu_millis;
207 self.memory_allocated += req.memory_mb;
208
209 for _ in 0..req.gpu_count {
211 if let Some(gpu) = self.gpus.iter()
212 .find(|g| !self.gpus_allocated.contains(&g.device_id)
213 && g.available_memory_mb() >= req.gpu_memory_mb)
214 {
215 self.gpus_allocated.push(gpu.device_id);
216 }
217 }
218
219 true
220 }
221
222 pub fn release(&mut self, req: &ResourceRequirements, gpu_ids: &[u32]) {
224 self.cpu_allocated = self.cpu_allocated.saturating_sub(req.cpu_millis);
225 self.memory_allocated = self.memory_allocated.saturating_sub(req.memory_mb);
226 self.gpus_allocated.retain(|id| !gpu_ids.contains(id));
227 }
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct Taint {
233 pub key: String,
235 pub value: String,
237 pub effect: TaintEffect,
239}
240
241#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
243pub enum TaintEffect {
244 NoSchedule,
246 PreferNoSchedule,
248 NoExecute,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct NodeCondition {
255 pub condition_type: String,
257 pub status: bool,
259 pub last_transition: chrono::DateTime<chrono::Utc>,
261 pub reason: Option<String>,
263 pub message: Option<String>,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct Workload {
270 pub id: String,
272 pub name: String,
274 pub namespace: String,
276 pub resources: ResourceRequirements,
278 pub priority: i32,
280 pub priority_class: Option<String>,
282 pub constraints: Vec<PlacementConstraint>,
284 pub affinity: Option<Affinity>,
286 pub tolerations: Vec<Toleration>,
288 pub preemption_policy: PreemptionPolicy,
290 pub created_at: chrono::DateTime<chrono::Utc>,
292}
293
294impl Workload {
295 pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
297 Self {
298 id: id.into(),
299 name: name.into(),
300 namespace: "default".to_string(),
301 resources: ResourceRequirements::default(),
302 priority: 0,
303 priority_class: None,
304 constraints: Vec::new(),
305 affinity: None,
306 tolerations: Vec::new(),
307 preemption_policy: PreemptionPolicy::PreemptLowerPriority,
308 created_at: chrono::Utc::now(),
309 }
310 }
311
312 pub fn with_resources(mut self, resources: ResourceRequirements) -> Self {
314 self.resources = resources;
315 self
316 }
317
318 pub fn with_priority(mut self, priority: i32) -> Self {
320 self.priority = priority;
321 self
322 }
323
324 pub fn with_constraint(mut self, constraint: PlacementConstraint) -> Self {
326 self.constraints.push(constraint);
327 self
328 }
329
330 pub fn with_affinity(mut self, affinity: Affinity) -> Self {
332 self.affinity = Some(affinity);
333 self
334 }
335
336 pub fn with_toleration(mut self, toleration: Toleration) -> Self {
338 self.tolerations.push(toleration);
339 self
340 }
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct Toleration {
346 pub key: Option<String>,
348 pub operator: TolerationOperator,
350 pub value: Option<String>,
352 pub effect: Option<TaintEffect>,
354 pub toleration_seconds: Option<u64>,
356}
357
358#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
360pub enum TolerationOperator {
361 Equal,
363 Exists,
365}
366
367#[derive(Debug, Clone)]
369pub struct SchedulingDecision {
370 pub workload_id: String,
372 pub node_id: Option<NodeId>,
374 pub score: f64,
376 pub reason: String,
378 pub preempted: Vec<String>,
380 pub latency_ms: u64,
382}
383
384pub struct Scheduler {
386 nodes: Arc<RwLock<HashMap<NodeId, NodeResources>>>,
388 queue: Arc<SchedulingQueue>,
390 algorithm: Arc<dyn SchedulingAlgorithm + Send + Sync>,
392 assignments: Arc<RwLock<HashMap<String, NodeId>>>,
394 priority_classes: Arc<RwLock<HashMap<String, PriorityClass>>>,
396}
397
398impl Scheduler {
399 pub fn new() -> Self {
401 Self {
402 nodes: Arc::new(RwLock::new(HashMap::new())),
403 queue: Arc::new(SchedulingQueue::new()),
404 algorithm: Arc::new(BinPackScheduler::new()),
405 assignments: Arc::new(RwLock::new(HashMap::new())),
406 priority_classes: Arc::new(RwLock::new(HashMap::new())),
407 }
408 }
409
410 pub fn with_algorithm<A: SchedulingAlgorithm + Send + Sync + 'static>(algorithm: A) -> Self {
412 Self {
413 nodes: Arc::new(RwLock::new(HashMap::new())),
414 queue: Arc::new(SchedulingQueue::new()),
415 algorithm: Arc::new(algorithm),
416 assignments: Arc::new(RwLock::new(HashMap::new())),
417 priority_classes: Arc::new(RwLock::new(HashMap::new())),
418 }
419 }
420
421 pub fn register_node(&self, node: NodeResources) {
423 info!(node_id = %node.node_id, "Registering node");
424 self.nodes.write().insert(node.node_id, node);
425 }
426
427 pub fn unregister_node(&self, node_id: &NodeId) {
429 info!(node_id = %node_id, "Unregistering node");
430 self.nodes.write().remove(node_id);
431 }
432
433 pub fn update_node(&self, node: NodeResources) {
435 self.nodes.write().insert(node.node_id, node);
436 }
437
438 pub fn node_count(&self) -> usize {
440 self.nodes.read().len()
441 }
442
443 pub fn submit(&self, workload: Workload) {
445 debug!(workload_id = %workload.id, "Submitting workload");
446 self.queue.enqueue(workload);
447 }
448
449 pub fn schedule_next(&self) -> Option<SchedulingDecision> {
451 let workload = self.queue.dequeue()?;
452 Some(self.schedule(&workload))
453 }
454
455 pub fn schedule(&self, workload: &Workload) -> SchedulingDecision {
457 let start = std::time::Instant::now();
458 let nodes = self.nodes.read();
459
460 let candidates: Vec<_> = nodes.values()
462 .filter(|n| n.can_fit(&workload.resources))
463 .filter(|n| self.check_constraints(workload, n))
464 .filter(|n| self.check_taints(workload, n))
465 .collect();
466
467 if candidates.is_empty() {
468 drop(nodes);
470 if let Some(decision) = self.try_preemption(workload) {
471 return decision;
472 }
473
474 return SchedulingDecision {
475 workload_id: workload.id.clone(),
476 node_id: None,
477 score: 0.0,
478 reason: "No suitable nodes available".to_string(),
479 preempted: Vec::new(),
480 latency_ms: start.elapsed().as_millis() as u64,
481 };
482 }
483
484 let scored: Vec<_> = candidates.iter()
486 .map(|n| (n, self.algorithm.score(workload, n)))
487 .collect();
488
489 let (best_node, score) = scored.iter()
490 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
491 .map(|(n, s)| (*n, *s))
492 .unwrap();
493
494 let node_id = best_node.node_id;
495 drop(nodes);
496
497 if let Some(node) = self.nodes.write().get_mut(&node_id) {
499 node.allocate(&workload.resources);
500 }
501
502 self.assignments.write().insert(workload.id.clone(), node_id);
503
504 info!(
505 workload_id = %workload.id,
506 node_id = %node_id,
507 score = score,
508 "Workload scheduled"
509 );
510
511 SchedulingDecision {
512 workload_id: workload.id.clone(),
513 node_id: Some(node_id),
514 score,
515 reason: "Scheduled successfully".to_string(),
516 preempted: Vec::new(),
517 latency_ms: start.elapsed().as_millis() as u64,
518 }
519 }
520
521 fn check_constraints(&self, workload: &Workload, node: &NodeResources) -> bool {
523 for constraint in &workload.constraints {
524 if !constraint.matches(node) {
525 return false;
526 }
527 }
528
529 if let Some(affinity) = &workload.affinity {
531 if !affinity.matches(node) {
532 return false;
533 }
534 }
535
536 true
537 }
538
539 fn check_taints(&self, workload: &Workload, node: &NodeResources) -> bool {
541 for taint in &node.taints {
542 let tolerated = workload.tolerations.iter().any(|t| {
543 let key_matches = t.key.as_ref().map(|k| k == &taint.key).unwrap_or(true);
545
546 let value_matches = match t.operator {
548 TolerationOperator::Exists => true,
549 TolerationOperator::Equal => {
550 t.value.as_ref().map(|v| v == &taint.value).unwrap_or(false)
551 }
552 };
553
554 let effect_matches = t.effect.map(|e| e == taint.effect).unwrap_or(true);
556
557 key_matches && value_matches && effect_matches
558 });
559
560 if !tolerated && taint.effect == TaintEffect::NoSchedule {
561 return false;
562 }
563 }
564
565 true
566 }
567
568 fn try_preemption(&self, workload: &Workload) -> Option<SchedulingDecision> {
570 if workload.preemption_policy == PreemptionPolicy::Never {
571 return None;
572 }
573
574 let assignments = self.assignments.read();
576 let mut nodes = self.nodes.write();
577
578 for (node_id, node) in nodes.iter_mut() {
579 let preemptable: Vec<_> = assignments.iter()
581 .filter(|(_, n)| *n == node_id)
582 .map(|(w, _)| w.clone())
583 .collect();
584
585 if node.can_fit(&workload.resources) || !preemptable.is_empty() {
588 warn!(
590 workload_id = %workload.id,
591 node_id = %node_id,
592 "Preemption would be required"
593 );
594 }
595 }
596
597 None
598 }
599
600 pub fn get_assignment(&self, workload_id: &str) -> Option<NodeId> {
602 self.assignments.read().get(workload_id).copied()
603 }
604
605 pub fn release(&self, workload_id: &str, resources: &ResourceRequirements, gpu_ids: &[u32]) {
607 if let Some(node_id) = self.assignments.write().remove(workload_id) {
608 if let Some(node) = self.nodes.write().get_mut(&node_id) {
609 node.release(resources, gpu_ids);
610 }
611 }
612 }
613
614 pub fn queue_len(&self) -> usize {
616 self.queue.len()
617 }
618
619 pub fn register_priority_class(&self, class: PriorityClass) {
621 self.priority_classes.write().insert(class.name.clone(), class);
622 }
623}
624
625impl Default for Scheduler {
626 fn default() -> Self {
627 Self::new()
628 }
629}
630
631#[cfg(test)]
632mod tests {
633 use super::*;
634
635 #[test]
636 fn test_node_resources() {
637 let mut node = NodeResources::new(NodeId::new(), 4000, 8192);
638
639 let req = ResourceRequirements::new().cpu(1000).memory(2048);
640 assert!(node.can_fit(&req));
641
642 assert!(node.allocate(&req));
643 assert_eq!(node.cpu_available(), 3000);
644 assert_eq!(node.memory_available(), 6144);
645 }
646
647 #[test]
648 fn test_scheduler_basic() {
649 let scheduler = Scheduler::new();
650
651 let node = NodeResources::new(NodeId::new(), 4000, 8192);
652 scheduler.register_node(node);
653
654 let workload = Workload::new("w1", "test")
655 .with_resources(ResourceRequirements::new().cpu(1000).memory(1024));
656
657 let decision = scheduler.schedule(&workload);
658 assert!(decision.node_id.is_some());
659 }
660
661 #[test]
662 fn test_scheduler_no_capacity() {
663 let scheduler = Scheduler::new();
664
665 let node = NodeResources::new(NodeId::new(), 1000, 1024);
666 scheduler.register_node(node);
667
668 let workload = Workload::new("w1", "test")
669 .with_resources(ResourceRequirements::new().cpu(2000).memory(2048));
670
671 let decision = scheduler.schedule(&workload);
672 assert!(decision.node_id.is_none());
673 }
674}