forge_orchestration/scheduler/
placement.rs1use super::NodeResources;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub enum PlacementConstraint {
14 NodeSelector {
16 key: String,
18 value: String,
20 },
21 Zone(String),
23 Region(String),
25 RequiresGpu,
27 GpuModel(String),
29 MinGpuMemory(u64),
31 RequiresTensorCores,
33 Architecture(String),
35 Expression(ConstraintExpression),
37}
38
39impl PlacementConstraint {
40 pub fn matches(&self, node: &NodeResources) -> bool {
42 match self {
43 PlacementConstraint::NodeSelector { key, value } => {
44 node.labels.get(key).map(|v| v == value).unwrap_or(false)
45 }
46 PlacementConstraint::Zone(zone) => {
47 node.labels.get("topology.kubernetes.io/zone")
48 .or_else(|| node.labels.get("zone"))
49 .map(|z| z == zone)
50 .unwrap_or(false)
51 }
52 PlacementConstraint::Region(region) => {
53 node.labels.get("topology.kubernetes.io/region")
54 .or_else(|| node.labels.get("region"))
55 .map(|r| r == region)
56 .unwrap_or(false)
57 }
58 PlacementConstraint::RequiresGpu => {
59 !node.gpus.is_empty() && node.gpus_available() > 0
60 }
61 PlacementConstraint::GpuModel(model) => {
62 node.gpus.iter().any(|g| g.model.contains(model))
63 }
64 PlacementConstraint::MinGpuMemory(min_mb) => {
65 node.gpus.iter().any(|g| g.memory_mb >= *min_mb)
66 }
67 PlacementConstraint::RequiresTensorCores => {
68 node.gpus.iter().any(|g| g.tensor_cores)
69 }
70 PlacementConstraint::Architecture(arch) => {
71 node.labels.get("kubernetes.io/arch")
72 .or_else(|| node.labels.get("arch"))
73 .map(|a| a == arch)
74 .unwrap_or(false)
75 }
76 PlacementConstraint::Expression(expr) => {
77 expr.evaluate(node)
78 }
79 }
80 }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct ConstraintExpression {
86 pub key: String,
88 pub operator: ExpressionOperator,
90 pub values: Vec<String>,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
96pub enum ExpressionOperator {
97 In,
99 NotIn,
101 Exists,
103 DoesNotExist,
105 Gt,
107 Lt,
109}
110
111impl ConstraintExpression {
112 pub fn evaluate(&self, node: &NodeResources) -> bool {
114 let value = node.labels.get(&self.key);
115
116 match self.operator {
117 ExpressionOperator::In => {
118 value.map(|v| self.values.contains(v)).unwrap_or(false)
119 }
120 ExpressionOperator::NotIn => {
121 value.map(|v| !self.values.contains(v)).unwrap_or(true)
122 }
123 ExpressionOperator::Exists => value.is_some(),
124 ExpressionOperator::DoesNotExist => value.is_none(),
125 ExpressionOperator::Gt => {
126 if let (Some(v), Some(threshold)) = (value, self.values.first()) {
127 v.parse::<i64>().ok()
128 .zip(threshold.parse::<i64>().ok())
129 .map(|(v, t)| v > t)
130 .unwrap_or(false)
131 } else {
132 false
133 }
134 }
135 ExpressionOperator::Lt => {
136 if let (Some(v), Some(threshold)) = (value, self.values.first()) {
137 v.parse::<i64>().ok()
138 .zip(threshold.parse::<i64>().ok())
139 .map(|(v, t)| v < t)
140 .unwrap_or(false)
141 } else {
142 false
143 }
144 }
145 }
146 }
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct Affinity {
152 pub node_affinity: Option<NodeAffinity>,
154 pub workload_affinity: Option<WorkloadAffinity>,
156 pub workload_anti_affinity: Option<WorkloadAffinity>,
158}
159
160impl Affinity {
161 pub fn new() -> Self {
163 Self {
164 node_affinity: None,
165 workload_affinity: None,
166 workload_anti_affinity: None,
167 }
168 }
169
170 pub fn with_node_affinity(mut self, affinity: NodeAffinity) -> Self {
172 self.node_affinity = Some(affinity);
173 self
174 }
175
176 pub fn with_workload_affinity(mut self, affinity: WorkloadAffinity) -> Self {
178 self.workload_affinity = Some(affinity);
179 self
180 }
181
182 pub fn with_workload_anti_affinity(mut self, affinity: WorkloadAffinity) -> Self {
184 self.workload_anti_affinity = Some(affinity);
185 self
186 }
187
188 pub fn matches(&self, node: &NodeResources) -> bool {
190 if let Some(node_affinity) = &self.node_affinity {
192 for rule in &node_affinity.required {
194 if !rule.matches(node) {
195 return false;
196 }
197 }
198 }
199
200 true
201 }
202}
203
204impl Default for Affinity {
205 fn default() -> Self {
206 Self::new()
207 }
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct NodeAffinity {
213 pub required: Vec<AffinityRule>,
215 pub preferred: Vec<WeightedAffinityRule>,
217}
218
219impl NodeAffinity {
220 pub fn new() -> Self {
222 Self {
223 required: Vec::new(),
224 preferred: Vec::new(),
225 }
226 }
227
228 pub fn require(mut self, rule: AffinityRule) -> Self {
230 self.required.push(rule);
231 self
232 }
233
234 pub fn prefer(mut self, weight: i32, rule: AffinityRule) -> Self {
236 self.preferred.push(WeightedAffinityRule { weight, rule });
237 self
238 }
239}
240
241impl Default for NodeAffinity {
242 fn default() -> Self {
243 Self::new()
244 }
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct WorkloadAffinity {
250 pub required: Vec<WorkloadAffinityTerm>,
252 pub preferred: Vec<WeightedWorkloadAffinityTerm>,
254}
255
256impl WorkloadAffinity {
257 pub fn new() -> Self {
259 Self {
260 required: Vec::new(),
261 preferred: Vec::new(),
262 }
263 }
264}
265
266impl Default for WorkloadAffinity {
267 fn default() -> Self {
268 Self::new()
269 }
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct WorkloadAffinityTerm {
275 pub label_selector: LabelSelector,
277 pub topology_key: String,
279 pub namespaces: Option<Vec<String>>,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct WeightedWorkloadAffinityTerm {
286 pub weight: i32,
288 pub term: WorkloadAffinityTerm,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct LabelSelector {
295 pub match_labels: std::collections::HashMap<String, String>,
297 pub match_expressions: Vec<ConstraintExpression>,
299}
300
301impl LabelSelector {
302 pub fn new() -> Self {
304 Self {
305 match_labels: std::collections::HashMap::new(),
306 match_expressions: Vec::new(),
307 }
308 }
309
310 pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
312 self.match_labels.insert(key.into(), value.into());
313 self
314 }
315
316 pub fn with_expression(mut self, expr: ConstraintExpression) -> Self {
318 self.match_expressions.push(expr);
319 self
320 }
321}
322
323impl Default for LabelSelector {
324 fn default() -> Self {
325 Self::new()
326 }
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct AffinityRule {
332 pub match_expressions: Vec<ConstraintExpression>,
334}
335
336impl AffinityRule {
337 pub fn new() -> Self {
339 Self {
340 match_expressions: Vec::new(),
341 }
342 }
343
344 pub fn with_expression(mut self, expr: ConstraintExpression) -> Self {
346 self.match_expressions.push(expr);
347 self
348 }
349
350 pub fn matches(&self, node: &NodeResources) -> bool {
352 self.match_expressions.iter().all(|expr| expr.evaluate(node))
353 }
354}
355
356impl Default for AffinityRule {
357 fn default() -> Self {
358 Self::new()
359 }
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct WeightedAffinityRule {
365 pub weight: i32,
367 pub rule: AffinityRule,
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374 use crate::types::{NodeId, GpuResources};
375
376 #[test]
377 fn test_node_selector() {
378 let mut node = NodeResources::new(NodeId::new(), 4000, 8192);
379 node.labels.insert("env".to_string(), "production".to_string());
380
381 let constraint = PlacementConstraint::NodeSelector {
382 key: "env".to_string(),
383 value: "production".to_string(),
384 };
385
386 assert!(constraint.matches(&node));
387
388 let wrong_constraint = PlacementConstraint::NodeSelector {
389 key: "env".to_string(),
390 value: "staging".to_string(),
391 };
392
393 assert!(!wrong_constraint.matches(&node));
394 }
395
396 #[test]
397 fn test_gpu_constraints() {
398 let gpu = GpuResources::new(0, "NVIDIA A100", 40960)
399 .with_tensor_cores(true)
400 .with_compute_capability(8.0);
401
402 let node = NodeResources::new(NodeId::new(), 4000, 8192)
403 .with_gpu(gpu);
404
405 assert!(PlacementConstraint::RequiresGpu.matches(&node));
406 assert!(PlacementConstraint::RequiresTensorCores.matches(&node));
407 assert!(PlacementConstraint::GpuModel("A100".to_string()).matches(&node));
408 assert!(PlacementConstraint::MinGpuMemory(40000).matches(&node));
409 assert!(!PlacementConstraint::MinGpuMemory(50000).matches(&node));
410 }
411
412 #[test]
413 fn test_expression_operators() {
414 let mut node = NodeResources::new(NodeId::new(), 4000, 8192);
415 node.labels.insert("tier".to_string(), "frontend".to_string());
416 node.labels.insert("priority".to_string(), "10".to_string());
417
418 let in_expr = ConstraintExpression {
419 key: "tier".to_string(),
420 operator: ExpressionOperator::In,
421 values: vec!["frontend".to_string(), "backend".to_string()],
422 };
423 assert!(in_expr.evaluate(&node));
424
425 let gt_expr = ConstraintExpression {
426 key: "priority".to_string(),
427 operator: ExpressionOperator::Gt,
428 values: vec!["5".to_string()],
429 };
430 assert!(gt_expr.evaluate(&node));
431
432 let exists_expr = ConstraintExpression {
433 key: "tier".to_string(),
434 operator: ExpressionOperator::Exists,
435 values: vec![],
436 };
437 assert!(exists_expr.evaluate(&node));
438 }
439}