1use crate::{GpuSpec, MachineProfile, Platform};
13use serde::{Deserialize, Serialize};
14
15pub fn parse_cpu(s: &str) -> Result<f64, String> {
23 let s = s.trim();
24 if s.is_empty() {
25 return Err("empty CPU string".to_string());
26 }
27
28 if let Some(millis) = s.strip_suffix('m') {
29 let v: f64 = millis
30 .parse()
31 .map_err(|_| format!("invalid CPU millicore value: '{s}'"))?;
32 Ok(v / 1000.0)
33 } else {
34 s.parse().map_err(|_| format!("invalid CPU value: '{s}'"))
35 }
36}
37
38pub fn parse_memory_bytes(s: &str) -> Result<u64, String> {
43 let s = s.trim();
44 if s.is_empty() {
45 return Err("empty memory/storage string".to_string());
46 }
47
48 for (suffix, multiplier) in [
49 ("Ti", 1024_f64.powi(4)),
50 ("Gi", 1024_f64.powi(3)),
51 ("Mi", 1024_f64.powi(2)),
52 ("Ki", 1024_f64),
53 ("T", 1_000_000_000_000_f64),
54 ("G", 1_000_000_000_f64),
55 ("M", 1_000_000_f64),
56 ("k", 1_000_f64),
57 ] {
58 if let Some(number) = s.strip_suffix(suffix) {
59 let value: f64 = number
60 .parse()
61 .map_err(|_| format!("invalid memory value: '{s}'"))?;
62 let bytes = value * multiplier;
63 if !bytes.is_finite() || bytes < 0.0 || bytes >= u64::MAX as f64 {
66 return Err(format!("memory value is out of range: '{s}'"));
67 }
68 return Ok(bytes as u64);
69 }
70 }
71 s.parse()
72 .map_err(|_| format!("invalid memory value: '{s}'"))
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum InstanceFamily {
82 Burstable,
83 GeneralPurpose,
84 ComputeOptimized,
85 MemoryOptimized,
86 StorageOptimized,
87 GpuCompute,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
92#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
93#[serde(rename_all = "snake_case")]
94pub enum Architecture {
95 Arm64,
96 X86_64,
97}
98
99pub fn default_architecture(platform: Platform) -> Option<Architecture> {
101 match platform {
102 Platform::Aws => Some(Architecture::Arm64),
103 Platform::Gcp | Platform::Azure => Some(Architecture::X86_64),
104 Platform::Kubernetes | Platform::Machines | Platform::Local | Platform::Test => None,
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub struct CatalogGpu {
111 pub gpu_type: &'static str,
112 pub count: u32,
113}
114
115#[derive(Debug, Clone)]
119pub struct InstanceTypeSpec {
120 pub name: &'static str,
121 pub platform: Platform,
122 pub family: InstanceFamily,
123 pub architecture: Architecture,
124 pub vcpu: u32,
126 pub memory_bytes: u64,
128 pub ephemeral_storage_bytes: u64,
130 pub gpu: Option<CatalogGpu>,
132}
133
134impl InstanceTypeSpec {
135 pub fn has_configurable_ephemeral_storage(&self) -> bool {
138 matches!(
139 self.family,
140 InstanceFamily::Burstable
141 | InstanceFamily::GeneralPurpose
142 | InstanceFamily::ComputeOptimized
143 | InstanceFamily::MemoryOptimized
144 )
145 }
146
147 pub fn is_nested_virt_capable(&self) -> bool {
153 match self.platform {
154 Platform::Aws => {
155 let name = self.name;
156 name.starts_with("m8i.")
157 || name.starts_with("c8i.")
158 || name.starts_with("r8i.")
159 || name.starts_with("m8i-flex.")
160 || name.starts_with("c8i-flex.")
161 || name.starts_with("r8i-flex.")
162 }
163 Platform::Gcp => self.name.starts_with("n2-standard-"),
164 Platform::Azure => {
165 let name = self.name;
166 (name.starts_with("Standard_D") && name.ends_with("s_v5"))
167 || (name.starts_with("Standard_E") && name.ends_with("s_v5"))
168 || (name.starts_with("Standard_F") && name.ends_with("s_v2"))
169 }
170 _ => false,
171 }
172 }
173
174 pub fn to_machine_profile(&self) -> MachineProfile {
176 MachineProfile {
177 cpu: format!("{}.0", self.vcpu),
178 memory_bytes: self.memory_bytes,
179 ephemeral_storage_bytes: self.ephemeral_storage_bytes,
180 architecture: Some(self.architecture),
181 gpu: self.gpu.map(|g| GpuSpec {
182 gpu_type: g.gpu_type.to_string(),
183 count: g.count,
184 }),
185 data_disk: None,
186 }
187 }
188
189 pub fn to_machine_profile_for_storage(&self, requested_bytes: u64) -> MachineProfile {
193 let mut profile = self.to_machine_profile();
194 if self.has_configurable_ephemeral_storage() {
195 profile.ephemeral_storage_bytes = profile.ephemeral_storage_bytes.max(requested_bytes);
196 }
197 profile
198 }
199}
200
201const KI: u64 = 1024;
203const MI: u64 = KI * 1024;
204const GI: u64 = MI * 1024;
205const TI: u64 = GI * 1024;
206
207pub fn max_configurable_ephemeral_storage_bytes(platform: Platform) -> Option<u64> {
213 let disk_limit_gib = match platform {
214 Platform::Aws | Platform::Gcp => 64 * 1024,
216 Platform::Azure => 4_095,
218 Platform::Kubernetes | Platform::Machines | Platform::Local | Platform::Test => {
219 return None;
220 }
221 };
222
223 let max_requested_gib = (disk_limit_gib - 12) * 4 / 5;
228 Some(max_requested_gib * GI)
229}
230
231static CATALOG: &[InstanceTypeSpec] = &[
239 InstanceTypeSpec {
245 name: "t4g.micro",
246 platform: Platform::Aws,
247 family: InstanceFamily::Burstable,
248 architecture: Architecture::Arm64,
249 vcpu: 2,
250 memory_bytes: 1 * GI,
251 ephemeral_storage_bytes: 20 * GI,
252 gpu: None,
253 },
254 InstanceTypeSpec {
255 name: "t4g.small",
256 platform: Platform::Aws,
257 family: InstanceFamily::Burstable,
258 architecture: Architecture::Arm64,
259 vcpu: 2,
260 memory_bytes: 2 * GI,
261 ephemeral_storage_bytes: 20 * GI,
262 gpu: None,
263 },
264 InstanceTypeSpec {
265 name: "t4g.medium",
266 platform: Platform::Aws,
267 family: InstanceFamily::Burstable,
268 architecture: Architecture::Arm64,
269 vcpu: 2,
270 memory_bytes: 4 * GI,
271 ephemeral_storage_bytes: 20 * GI,
272 gpu: None,
273 },
274 InstanceTypeSpec {
275 name: "t4g.large",
276 platform: Platform::Aws,
277 family: InstanceFamily::Burstable,
278 architecture: Architecture::Arm64,
279 vcpu: 2,
280 memory_bytes: 8 * GI,
281 ephemeral_storage_bytes: 20 * GI,
282 gpu: None,
283 },
284 InstanceTypeSpec {
285 name: "t3.xlarge",
286 platform: Platform::Aws,
287 family: InstanceFamily::Burstable,
288 architecture: Architecture::X86_64,
289 vcpu: 4,
290 memory_bytes: 16 * GI,
291 ephemeral_storage_bytes: 20 * GI,
292 gpu: None,
293 },
294 InstanceTypeSpec {
295 name: "t4g.xlarge",
296 platform: Platform::Aws,
297 family: InstanceFamily::Burstable,
298 architecture: Architecture::Arm64,
299 vcpu: 4,
300 memory_bytes: 16 * GI,
301 ephemeral_storage_bytes: 20 * GI,
302 gpu: None,
303 },
304 InstanceTypeSpec {
306 name: "m7g.medium",
307 platform: Platform::Aws,
308 family: InstanceFamily::GeneralPurpose,
309 architecture: Architecture::Arm64,
310 vcpu: 1,
311 memory_bytes: 4 * GI,
312 ephemeral_storage_bytes: 20 * GI,
313 gpu: None,
314 },
315 InstanceTypeSpec {
316 name: "m7i.large",
317 platform: Platform::Aws,
318 family: InstanceFamily::GeneralPurpose,
319 architecture: Architecture::X86_64,
320 vcpu: 2,
321 memory_bytes: 8 * GI,
322 ephemeral_storage_bytes: 20 * GI,
323 gpu: None,
324 },
325 InstanceTypeSpec {
326 name: "m7g.large",
327 platform: Platform::Aws,
328 family: InstanceFamily::GeneralPurpose,
329 architecture: Architecture::Arm64,
330 vcpu: 2,
331 memory_bytes: 8 * GI,
332 ephemeral_storage_bytes: 20 * GI,
333 gpu: None,
334 },
335 InstanceTypeSpec {
343 name: "m8i.large",
344 platform: Platform::Aws,
345 family: InstanceFamily::GeneralPurpose,
346 architecture: Architecture::X86_64,
347 vcpu: 2,
348 memory_bytes: 8 * GI,
349 ephemeral_storage_bytes: 20 * GI,
350 gpu: None,
351 },
352 InstanceTypeSpec {
353 name: "m7i.xlarge",
354 platform: Platform::Aws,
355 family: InstanceFamily::GeneralPurpose,
356 architecture: Architecture::X86_64,
357 vcpu: 4,
358 memory_bytes: 16 * GI,
359 ephemeral_storage_bytes: 20 * GI,
360 gpu: None,
361 },
362 InstanceTypeSpec {
363 name: "m7g.xlarge",
364 platform: Platform::Aws,
365 family: InstanceFamily::GeneralPurpose,
366 architecture: Architecture::Arm64,
367 vcpu: 4,
368 memory_bytes: 16 * GI,
369 ephemeral_storage_bytes: 20 * GI,
370 gpu: None,
371 },
372 InstanceTypeSpec {
373 name: "m8i.xlarge",
374 platform: Platform::Aws,
375 family: InstanceFamily::GeneralPurpose,
376 architecture: Architecture::X86_64,
377 vcpu: 4,
378 memory_bytes: 16 * GI,
379 ephemeral_storage_bytes: 20 * GI,
380 gpu: None,
381 },
382 InstanceTypeSpec {
383 name: "m7i.2xlarge",
384 platform: Platform::Aws,
385 family: InstanceFamily::GeneralPurpose,
386 architecture: Architecture::X86_64,
387 vcpu: 8,
388 memory_bytes: 32 * GI,
389 ephemeral_storage_bytes: 20 * GI,
390 gpu: None,
391 },
392 InstanceTypeSpec {
393 name: "m7g.2xlarge",
394 platform: Platform::Aws,
395 family: InstanceFamily::GeneralPurpose,
396 architecture: Architecture::Arm64,
397 vcpu: 8,
398 memory_bytes: 32 * GI,
399 ephemeral_storage_bytes: 20 * GI,
400 gpu: None,
401 },
402 InstanceTypeSpec {
403 name: "m8i.2xlarge",
404 platform: Platform::Aws,
405 family: InstanceFamily::GeneralPurpose,
406 architecture: Architecture::X86_64,
407 vcpu: 8,
408 memory_bytes: 32 * GI,
409 ephemeral_storage_bytes: 20 * GI,
410 gpu: None,
411 },
412 InstanceTypeSpec {
413 name: "m7i.4xlarge",
414 platform: Platform::Aws,
415 family: InstanceFamily::GeneralPurpose,
416 architecture: Architecture::X86_64,
417 vcpu: 16,
418 memory_bytes: 64 * GI,
419 ephemeral_storage_bytes: 20 * GI,
420 gpu: None,
421 },
422 InstanceTypeSpec {
423 name: "m7g.4xlarge",
424 platform: Platform::Aws,
425 family: InstanceFamily::GeneralPurpose,
426 architecture: Architecture::Arm64,
427 vcpu: 16,
428 memory_bytes: 64 * GI,
429 ephemeral_storage_bytes: 20 * GI,
430 gpu: None,
431 },
432 InstanceTypeSpec {
433 name: "m8i.4xlarge",
434 platform: Platform::Aws,
435 family: InstanceFamily::GeneralPurpose,
436 architecture: Architecture::X86_64,
437 vcpu: 16,
438 memory_bytes: 64 * GI,
439 ephemeral_storage_bytes: 20 * GI,
440 gpu: None,
441 },
442 InstanceTypeSpec {
444 name: "c7g.medium",
445 platform: Platform::Aws,
446 family: InstanceFamily::ComputeOptimized,
447 architecture: Architecture::Arm64,
448 vcpu: 1,
449 memory_bytes: 2 * GI,
450 ephemeral_storage_bytes: 20 * GI,
451 gpu: None,
452 },
453 InstanceTypeSpec {
454 name: "c8g.medium",
455 platform: Platform::Aws,
456 family: InstanceFamily::ComputeOptimized,
457 architecture: Architecture::Arm64,
458 vcpu: 1,
459 memory_bytes: 2 * GI,
460 ephemeral_storage_bytes: 20 * GI,
461 gpu: None,
462 },
463 InstanceTypeSpec {
464 name: "c7g.large",
465 platform: Platform::Aws,
466 family: InstanceFamily::ComputeOptimized,
467 architecture: Architecture::Arm64,
468 vcpu: 2,
469 memory_bytes: 4 * GI,
470 ephemeral_storage_bytes: 20 * GI,
471 gpu: None,
472 },
473 InstanceTypeSpec {
474 name: "c8g.large",
475 platform: Platform::Aws,
476 family: InstanceFamily::ComputeOptimized,
477 architecture: Architecture::Arm64,
478 vcpu: 2,
479 memory_bytes: 4 * GI,
480 ephemeral_storage_bytes: 20 * GI,
481 gpu: None,
482 },
483 InstanceTypeSpec {
484 name: "c8i.large",
485 platform: Platform::Aws,
486 family: InstanceFamily::ComputeOptimized,
487 architecture: Architecture::X86_64,
488 vcpu: 2,
489 memory_bytes: 4 * GI,
490 ephemeral_storage_bytes: 20 * GI,
491 gpu: None,
492 },
493 InstanceTypeSpec {
494 name: "c7g.xlarge",
495 platform: Platform::Aws,
496 family: InstanceFamily::ComputeOptimized,
497 architecture: Architecture::Arm64,
498 vcpu: 4,
499 memory_bytes: 8 * GI,
500 ephemeral_storage_bytes: 20 * GI,
501 gpu: None,
502 },
503 InstanceTypeSpec {
504 name: "c8g.xlarge",
505 platform: Platform::Aws,
506 family: InstanceFamily::ComputeOptimized,
507 architecture: Architecture::Arm64,
508 vcpu: 4,
509 memory_bytes: 8 * GI,
510 ephemeral_storage_bytes: 20 * GI,
511 gpu: None,
512 },
513 InstanceTypeSpec {
514 name: "c8i.xlarge",
515 platform: Platform::Aws,
516 family: InstanceFamily::ComputeOptimized,
517 architecture: Architecture::X86_64,
518 vcpu: 4,
519 memory_bytes: 8 * GI,
520 ephemeral_storage_bytes: 20 * GI,
521 gpu: None,
522 },
523 InstanceTypeSpec {
524 name: "c7g.2xlarge",
525 platform: Platform::Aws,
526 family: InstanceFamily::ComputeOptimized,
527 architecture: Architecture::Arm64,
528 vcpu: 8,
529 memory_bytes: 16 * GI,
530 ephemeral_storage_bytes: 20 * GI,
531 gpu: None,
532 },
533 InstanceTypeSpec {
534 name: "c8g.2xlarge",
535 platform: Platform::Aws,
536 family: InstanceFamily::ComputeOptimized,
537 architecture: Architecture::Arm64,
538 vcpu: 8,
539 memory_bytes: 16 * GI,
540 ephemeral_storage_bytes: 20 * GI,
541 gpu: None,
542 },
543 InstanceTypeSpec {
544 name: "c8i.2xlarge",
545 platform: Platform::Aws,
546 family: InstanceFamily::ComputeOptimized,
547 architecture: Architecture::X86_64,
548 vcpu: 8,
549 memory_bytes: 16 * GI,
550 ephemeral_storage_bytes: 20 * GI,
551 gpu: None,
552 },
553 InstanceTypeSpec {
554 name: "c7g.4xlarge",
555 platform: Platform::Aws,
556 family: InstanceFamily::ComputeOptimized,
557 architecture: Architecture::Arm64,
558 vcpu: 16,
559 memory_bytes: 32 * GI,
560 ephemeral_storage_bytes: 20 * GI,
561 gpu: None,
562 },
563 InstanceTypeSpec {
564 name: "c8g.4xlarge",
565 platform: Platform::Aws,
566 family: InstanceFamily::ComputeOptimized,
567 architecture: Architecture::Arm64,
568 vcpu: 16,
569 memory_bytes: 32 * GI,
570 ephemeral_storage_bytes: 20 * GI,
571 gpu: None,
572 },
573 InstanceTypeSpec {
574 name: "c8i.4xlarge",
575 platform: Platform::Aws,
576 family: InstanceFamily::ComputeOptimized,
577 architecture: Architecture::X86_64,
578 vcpu: 16,
579 memory_bytes: 32 * GI,
580 ephemeral_storage_bytes: 20 * GI,
581 gpu: None,
582 },
583 InstanceTypeSpec {
585 name: "r7g.medium",
586 platform: Platform::Aws,
587 family: InstanceFamily::MemoryOptimized,
588 architecture: Architecture::Arm64,
589 vcpu: 1,
590 memory_bytes: 8 * GI,
591 ephemeral_storage_bytes: 20 * GI,
592 gpu: None,
593 },
594 InstanceTypeSpec {
595 name: "r7g.large",
596 platform: Platform::Aws,
597 family: InstanceFamily::MemoryOptimized,
598 architecture: Architecture::Arm64,
599 vcpu: 2,
600 memory_bytes: 16 * GI,
601 ephemeral_storage_bytes: 20 * GI,
602 gpu: None,
603 },
604 InstanceTypeSpec {
605 name: "r6a.large",
606 platform: Platform::Aws,
607 family: InstanceFamily::MemoryOptimized,
608 architecture: Architecture::X86_64,
609 vcpu: 2,
610 memory_bytes: 16 * GI,
611 ephemeral_storage_bytes: 20 * GI,
612 gpu: None,
613 },
614 InstanceTypeSpec {
615 name: "r7i.large",
616 platform: Platform::Aws,
617 family: InstanceFamily::MemoryOptimized,
618 architecture: Architecture::X86_64,
619 vcpu: 2,
620 memory_bytes: 16 * GI,
621 ephemeral_storage_bytes: 20 * GI,
622 gpu: None,
623 },
624 InstanceTypeSpec {
625 name: "r7g.xlarge",
626 platform: Platform::Aws,
627 family: InstanceFamily::MemoryOptimized,
628 architecture: Architecture::Arm64,
629 vcpu: 4,
630 memory_bytes: 32 * GI,
631 ephemeral_storage_bytes: 20 * GI,
632 gpu: None,
633 },
634 InstanceTypeSpec {
635 name: "r6a.xlarge",
636 platform: Platform::Aws,
637 family: InstanceFamily::MemoryOptimized,
638 architecture: Architecture::X86_64,
639 vcpu: 4,
640 memory_bytes: 32 * GI,
641 ephemeral_storage_bytes: 20 * GI,
642 gpu: None,
643 },
644 InstanceTypeSpec {
645 name: "r7i.xlarge",
646 platform: Platform::Aws,
647 family: InstanceFamily::MemoryOptimized,
648 architecture: Architecture::X86_64,
649 vcpu: 4,
650 memory_bytes: 32 * GI,
651 ephemeral_storage_bytes: 20 * GI,
652 gpu: None,
653 },
654 InstanceTypeSpec {
655 name: "r7g.2xlarge",
656 platform: Platform::Aws,
657 family: InstanceFamily::MemoryOptimized,
658 architecture: Architecture::Arm64,
659 vcpu: 8,
660 memory_bytes: 64 * GI,
661 ephemeral_storage_bytes: 20 * GI,
662 gpu: None,
663 },
664 InstanceTypeSpec {
665 name: "r6a.2xlarge",
666 platform: Platform::Aws,
667 family: InstanceFamily::MemoryOptimized,
668 architecture: Architecture::X86_64,
669 vcpu: 8,
670 memory_bytes: 64 * GI,
671 ephemeral_storage_bytes: 20 * GI,
672 gpu: None,
673 },
674 InstanceTypeSpec {
675 name: "r7i.2xlarge",
676 platform: Platform::Aws,
677 family: InstanceFamily::MemoryOptimized,
678 architecture: Architecture::X86_64,
679 vcpu: 8,
680 memory_bytes: 64 * GI,
681 ephemeral_storage_bytes: 20 * GI,
682 gpu: None,
683 },
684 InstanceTypeSpec {
685 name: "r7g.4xlarge",
686 platform: Platform::Aws,
687 family: InstanceFamily::MemoryOptimized,
688 architecture: Architecture::Arm64,
689 vcpu: 16,
690 memory_bytes: 128 * GI,
691 ephemeral_storage_bytes: 20 * GI,
692 gpu: None,
693 },
694 InstanceTypeSpec {
695 name: "r6a.4xlarge",
696 platform: Platform::Aws,
697 family: InstanceFamily::MemoryOptimized,
698 architecture: Architecture::X86_64,
699 vcpu: 16,
700 memory_bytes: 128 * GI,
701 ephemeral_storage_bytes: 20 * GI,
702 gpu: None,
703 },
704 InstanceTypeSpec {
705 name: "r7i.4xlarge",
706 platform: Platform::Aws,
707 family: InstanceFamily::MemoryOptimized,
708 architecture: Architecture::X86_64,
709 vcpu: 16,
710 memory_bytes: 128 * GI,
711 ephemeral_storage_bytes: 20 * GI,
712 gpu: None,
713 },
714 InstanceTypeSpec {
716 name: "i4i.xlarge",
717 platform: Platform::Aws,
718 family: InstanceFamily::StorageOptimized,
719 architecture: Architecture::X86_64,
720 vcpu: 4,
721 memory_bytes: 32 * GI,
722 ephemeral_storage_bytes: 937 * GI,
723 gpu: None,
724 },
725 InstanceTypeSpec {
726 name: "i4i.2xlarge",
727 platform: Platform::Aws,
728 family: InstanceFamily::StorageOptimized,
729 architecture: Architecture::X86_64,
730 vcpu: 8,
731 memory_bytes: 64 * GI,
732 ephemeral_storage_bytes: 1875 * GI,
733 gpu: None,
734 },
735 InstanceTypeSpec {
736 name: "i4i.4xlarge",
737 platform: Platform::Aws,
738 family: InstanceFamily::StorageOptimized,
739 architecture: Architecture::X86_64,
740 vcpu: 16,
741 memory_bytes: 128 * GI,
742 ephemeral_storage_bytes: 3750 * GI,
743 gpu: None,
744 },
745 InstanceTypeSpec {
746 name: "i4i.8xlarge",
747 platform: Platform::Aws,
748 family: InstanceFamily::StorageOptimized,
749 architecture: Architecture::X86_64,
750 vcpu: 32,
751 memory_bytes: 256 * GI,
752 ephemeral_storage_bytes: 7500 * GI,
753 gpu: None,
754 },
755 InstanceTypeSpec {
757 name: "g5.xlarge",
758 platform: Platform::Aws,
759 family: InstanceFamily::GpuCompute,
760 architecture: Architecture::X86_64,
761 vcpu: 4,
762 memory_bytes: 16 * GI,
763 ephemeral_storage_bytes: 250 * GI,
764 gpu: Some(CatalogGpu {
765 gpu_type: "nvidia-t4",
766 count: 1,
767 }),
768 },
769 InstanceTypeSpec {
770 name: "g5.2xlarge",
771 platform: Platform::Aws,
772 family: InstanceFamily::GpuCompute,
773 architecture: Architecture::X86_64,
774 vcpu: 8,
775 memory_bytes: 32 * GI,
776 ephemeral_storage_bytes: 450 * GI,
777 gpu: Some(CatalogGpu {
778 gpu_type: "nvidia-t4",
779 count: 1,
780 }),
781 },
782 InstanceTypeSpec {
784 name: "p4d.24xlarge",
785 platform: Platform::Aws,
786 family: InstanceFamily::GpuCompute,
787 architecture: Architecture::X86_64,
788 vcpu: 96,
789 memory_bytes: 1152 * GI,
790 ephemeral_storage_bytes: 8000 * GI,
791 gpu: Some(CatalogGpu {
792 gpu_type: "nvidia-a100",
793 count: 8,
794 }),
795 },
796 InstanceTypeSpec {
798 name: "p5.48xlarge",
799 platform: Platform::Aws,
800 family: InstanceFamily::GpuCompute,
801 architecture: Architecture::X86_64,
802 vcpu: 192,
803 memory_bytes: 2048 * GI,
804 ephemeral_storage_bytes: 8000 * GI,
805 gpu: Some(CatalogGpu {
806 gpu_type: "nvidia-h100",
807 count: 8,
808 }),
809 },
810 InstanceTypeSpec {
816 name: "e2-micro",
817 platform: Platform::Gcp,
818 family: InstanceFamily::Burstable,
819 architecture: Architecture::X86_64,
820 vcpu: 2,
821 memory_bytes: 1 * GI,
822 ephemeral_storage_bytes: 20 * GI,
823 gpu: None,
824 },
825 InstanceTypeSpec {
826 name: "e2-small",
827 platform: Platform::Gcp,
828 family: InstanceFamily::Burstable,
829 architecture: Architecture::X86_64,
830 vcpu: 2,
831 memory_bytes: 2 * GI,
832 ephemeral_storage_bytes: 20 * GI,
833 gpu: None,
834 },
835 InstanceTypeSpec {
836 name: "e2-medium",
837 platform: Platform::Gcp,
838 family: InstanceFamily::Burstable,
839 architecture: Architecture::X86_64,
840 vcpu: 2,
841 memory_bytes: 4 * GI,
842 ephemeral_storage_bytes: 20 * GI,
843 gpu: None,
844 },
845 InstanceTypeSpec {
847 name: "n2-standard-2",
848 platform: Platform::Gcp,
849 family: InstanceFamily::GeneralPurpose,
850 architecture: Architecture::X86_64,
851 vcpu: 2,
852 memory_bytes: 8 * GI,
853 ephemeral_storage_bytes: 20 * GI,
854 gpu: None,
855 },
856 InstanceTypeSpec {
857 name: "n2-standard-4",
858 platform: Platform::Gcp,
859 family: InstanceFamily::GeneralPurpose,
860 architecture: Architecture::X86_64,
861 vcpu: 4,
862 memory_bytes: 16 * GI,
863 ephemeral_storage_bytes: 20 * GI,
864 gpu: None,
865 },
866 InstanceTypeSpec {
867 name: "n2-standard-8",
868 platform: Platform::Gcp,
869 family: InstanceFamily::GeneralPurpose,
870 architecture: Architecture::X86_64,
871 vcpu: 8,
872 memory_bytes: 32 * GI,
873 ephemeral_storage_bytes: 20 * GI,
874 gpu: None,
875 },
876 InstanceTypeSpec {
877 name: "n2-standard-16",
878 platform: Platform::Gcp,
879 family: InstanceFamily::GeneralPurpose,
880 architecture: Architecture::X86_64,
881 vcpu: 16,
882 memory_bytes: 64 * GI,
883 ephemeral_storage_bytes: 20 * GI,
884 gpu: None,
885 },
886 InstanceTypeSpec {
888 name: "c3-standard-4",
889 platform: Platform::Gcp,
890 family: InstanceFamily::ComputeOptimized,
891 architecture: Architecture::X86_64,
892 vcpu: 4,
893 memory_bytes: 8 * GI,
894 ephemeral_storage_bytes: 20 * GI,
895 gpu: None,
896 },
897 InstanceTypeSpec {
898 name: "c3-standard-8",
899 platform: Platform::Gcp,
900 family: InstanceFamily::ComputeOptimized,
901 architecture: Architecture::X86_64,
902 vcpu: 8,
903 memory_bytes: 16 * GI,
904 ephemeral_storage_bytes: 20 * GI,
905 gpu: None,
906 },
907 InstanceTypeSpec {
909 name: "n2-highmem-2",
910 platform: Platform::Gcp,
911 family: InstanceFamily::MemoryOptimized,
912 architecture: Architecture::X86_64,
913 vcpu: 2,
914 memory_bytes: 16 * GI,
915 ephemeral_storage_bytes: 20 * GI,
916 gpu: None,
917 },
918 InstanceTypeSpec {
919 name: "n2-highmem-4",
920 platform: Platform::Gcp,
921 family: InstanceFamily::MemoryOptimized,
922 architecture: Architecture::X86_64,
923 vcpu: 4,
924 memory_bytes: 32 * GI,
925 ephemeral_storage_bytes: 20 * GI,
926 gpu: None,
927 },
928 InstanceTypeSpec {
929 name: "n2-highmem-8",
930 platform: Platform::Gcp,
931 family: InstanceFamily::MemoryOptimized,
932 architecture: Architecture::X86_64,
933 vcpu: 8,
934 memory_bytes: 64 * GI,
935 ephemeral_storage_bytes: 20 * GI,
936 gpu: None,
937 },
938 InstanceTypeSpec {
939 name: "n2-highmem-16",
940 platform: Platform::Gcp,
941 family: InstanceFamily::MemoryOptimized,
942 architecture: Architecture::X86_64,
943 vcpu: 16,
944 memory_bytes: 128 * GI,
945 ephemeral_storage_bytes: 20 * GI,
946 gpu: None,
947 },
948 InstanceTypeSpec {
949 name: "n2-highmem-32",
950 platform: Platform::Gcp,
951 family: InstanceFamily::MemoryOptimized,
952 architecture: Architecture::X86_64,
953 vcpu: 32,
954 memory_bytes: 256 * GI,
955 ephemeral_storage_bytes: 20 * GI,
956 gpu: None,
957 },
958 InstanceTypeSpec {
960 name: "c3d-standard-8",
961 platform: Platform::Gcp,
962 family: InstanceFamily::StorageOptimized,
963 architecture: Architecture::X86_64,
964 vcpu: 8,
965 memory_bytes: 32 * GI,
966 ephemeral_storage_bytes: 480 * GI,
967 gpu: None,
968 },
969 InstanceTypeSpec {
970 name: "c3d-standard-16",
971 platform: Platform::Gcp,
972 family: InstanceFamily::StorageOptimized,
973 architecture: Architecture::X86_64,
974 vcpu: 16,
975 memory_bytes: 64 * GI,
976 ephemeral_storage_bytes: 960 * GI,
977 gpu: None,
978 },
979 InstanceTypeSpec {
980 name: "c3d-standard-30",
981 platform: Platform::Gcp,
982 family: InstanceFamily::StorageOptimized,
983 architecture: Architecture::X86_64,
984 vcpu: 30,
985 memory_bytes: 120 * GI,
986 ephemeral_storage_bytes: 1920 * GI,
987 gpu: None,
988 },
989 InstanceTypeSpec {
991 name: "n1-standard-4-t4",
992 platform: Platform::Gcp,
993 family: InstanceFamily::GpuCompute,
994 architecture: Architecture::X86_64,
995 vcpu: 4,
996 memory_bytes: 15 * GI,
997 ephemeral_storage_bytes: 100 * GI,
998 gpu: Some(CatalogGpu {
999 gpu_type: "nvidia-t4",
1000 count: 1,
1001 }),
1002 },
1003 InstanceTypeSpec {
1005 name: "a2-highgpu-1g",
1006 platform: Platform::Gcp,
1007 family: InstanceFamily::GpuCompute,
1008 architecture: Architecture::X86_64,
1009 vcpu: 12,
1010 memory_bytes: 85 * GI,
1011 ephemeral_storage_bytes: 100 * GI,
1012 gpu: Some(CatalogGpu {
1013 gpu_type: "nvidia-a100",
1014 count: 1,
1015 }),
1016 },
1017 InstanceTypeSpec {
1018 name: "a2-highgpu-8g",
1019 platform: Platform::Gcp,
1020 family: InstanceFamily::GpuCompute,
1021 architecture: Architecture::X86_64,
1022 vcpu: 96,
1023 memory_bytes: 1360 * GI,
1024 ephemeral_storage_bytes: 100 * GI,
1025 gpu: Some(CatalogGpu {
1026 gpu_type: "nvidia-a100",
1027 count: 8,
1028 }),
1029 },
1030 InstanceTypeSpec {
1032 name: "a3-highgpu-8g",
1033 platform: Platform::Gcp,
1034 family: InstanceFamily::GpuCompute,
1035 architecture: Architecture::X86_64,
1036 vcpu: 208,
1037 memory_bytes: 1872 * GI,
1038 ephemeral_storage_bytes: 100 * GI,
1039 gpu: Some(CatalogGpu {
1040 gpu_type: "nvidia-h100",
1041 count: 8,
1042 }),
1043 },
1044 InstanceTypeSpec {
1050 name: "Standard_B1s",
1051 platform: Platform::Azure,
1052 family: InstanceFamily::Burstable,
1053 architecture: Architecture::X86_64,
1054 vcpu: 1,
1055 memory_bytes: 1 * GI,
1056 ephemeral_storage_bytes: 20 * GI,
1057 gpu: None,
1058 },
1059 InstanceTypeSpec {
1060 name: "Standard_B2s",
1061 platform: Platform::Azure,
1062 family: InstanceFamily::Burstable,
1063 architecture: Architecture::X86_64,
1064 vcpu: 2,
1065 memory_bytes: 4 * GI,
1066 ephemeral_storage_bytes: 20 * GI,
1067 gpu: None,
1068 },
1069 InstanceTypeSpec {
1070 name: "Standard_B2ms",
1071 platform: Platform::Azure,
1072 family: InstanceFamily::Burstable,
1073 architecture: Architecture::X86_64,
1074 vcpu: 2,
1075 memory_bytes: 8 * GI,
1076 ephemeral_storage_bytes: 20 * GI,
1077 gpu: None,
1078 },
1079 InstanceTypeSpec {
1080 name: "Standard_B4ms",
1081 platform: Platform::Azure,
1082 family: InstanceFamily::Burstable,
1083 architecture: Architecture::X86_64,
1084 vcpu: 4,
1085 memory_bytes: 16 * GI,
1086 ephemeral_storage_bytes: 20 * GI,
1087 gpu: None,
1088 },
1089 InstanceTypeSpec {
1091 name: "Standard_D2s_v4",
1092 platform: Platform::Azure,
1093 family: InstanceFamily::GeneralPurpose,
1094 architecture: Architecture::X86_64,
1095 vcpu: 2,
1096 memory_bytes: 8 * GI,
1097 ephemeral_storage_bytes: 20 * GI,
1098 gpu: None,
1099 },
1100 InstanceTypeSpec {
1101 name: "Standard_D2s_v5",
1102 platform: Platform::Azure,
1103 family: InstanceFamily::GeneralPurpose,
1104 architecture: Architecture::X86_64,
1105 vcpu: 2,
1106 memory_bytes: 8 * GI,
1107 ephemeral_storage_bytes: 20 * GI,
1108 gpu: None,
1109 },
1110 InstanceTypeSpec {
1111 name: "Standard_D4s_v5",
1112 platform: Platform::Azure,
1113 family: InstanceFamily::GeneralPurpose,
1114 architecture: Architecture::X86_64,
1115 vcpu: 4,
1116 memory_bytes: 16 * GI,
1117 ephemeral_storage_bytes: 20 * GI,
1118 gpu: None,
1119 },
1120 InstanceTypeSpec {
1121 name: "Standard_D8s_v5",
1122 platform: Platform::Azure,
1123 family: InstanceFamily::GeneralPurpose,
1124 architecture: Architecture::X86_64,
1125 vcpu: 8,
1126 memory_bytes: 32 * GI,
1127 ephemeral_storage_bytes: 20 * GI,
1128 gpu: None,
1129 },
1130 InstanceTypeSpec {
1131 name: "Standard_D16s_v5",
1132 platform: Platform::Azure,
1133 family: InstanceFamily::GeneralPurpose,
1134 architecture: Architecture::X86_64,
1135 vcpu: 16,
1136 memory_bytes: 64 * GI,
1137 ephemeral_storage_bytes: 20 * GI,
1138 gpu: None,
1139 },
1140 InstanceTypeSpec {
1142 name: "Standard_F2s_v2",
1143 platform: Platform::Azure,
1144 family: InstanceFamily::ComputeOptimized,
1145 architecture: Architecture::X86_64,
1146 vcpu: 2,
1147 memory_bytes: 4 * GI,
1148 ephemeral_storage_bytes: 20 * GI,
1149 gpu: None,
1150 },
1151 InstanceTypeSpec {
1152 name: "Standard_F4s_v2",
1153 platform: Platform::Azure,
1154 family: InstanceFamily::ComputeOptimized,
1155 architecture: Architecture::X86_64,
1156 vcpu: 4,
1157 memory_bytes: 8 * GI,
1158 ephemeral_storage_bytes: 20 * GI,
1159 gpu: None,
1160 },
1161 InstanceTypeSpec {
1162 name: "Standard_F8s_v2",
1163 platform: Platform::Azure,
1164 family: InstanceFamily::ComputeOptimized,
1165 architecture: Architecture::X86_64,
1166 vcpu: 8,
1167 memory_bytes: 16 * GI,
1168 ephemeral_storage_bytes: 20 * GI,
1169 gpu: None,
1170 },
1171 InstanceTypeSpec {
1172 name: "Standard_F16s_v2",
1173 platform: Platform::Azure,
1174 family: InstanceFamily::ComputeOptimized,
1175 architecture: Architecture::X86_64,
1176 vcpu: 16,
1177 memory_bytes: 32 * GI,
1178 ephemeral_storage_bytes: 20 * GI,
1179 gpu: None,
1180 },
1181 InstanceTypeSpec {
1183 name: "Standard_E2s_v5",
1184 platform: Platform::Azure,
1185 family: InstanceFamily::MemoryOptimized,
1186 architecture: Architecture::X86_64,
1187 vcpu: 2,
1188 memory_bytes: 16 * GI,
1189 ephemeral_storage_bytes: 20 * GI,
1190 gpu: None,
1191 },
1192 InstanceTypeSpec {
1193 name: "Standard_E4s_v5",
1194 platform: Platform::Azure,
1195 family: InstanceFamily::MemoryOptimized,
1196 architecture: Architecture::X86_64,
1197 vcpu: 4,
1198 memory_bytes: 32 * GI,
1199 ephemeral_storage_bytes: 20 * GI,
1200 gpu: None,
1201 },
1202 InstanceTypeSpec {
1203 name: "Standard_E8s_v5",
1204 platform: Platform::Azure,
1205 family: InstanceFamily::MemoryOptimized,
1206 architecture: Architecture::X86_64,
1207 vcpu: 8,
1208 memory_bytes: 64 * GI,
1209 ephemeral_storage_bytes: 20 * GI,
1210 gpu: None,
1211 },
1212 InstanceTypeSpec {
1213 name: "Standard_E16s_v5",
1214 platform: Platform::Azure,
1215 family: InstanceFamily::MemoryOptimized,
1216 architecture: Architecture::X86_64,
1217 vcpu: 16,
1218 memory_bytes: 128 * GI,
1219 ephemeral_storage_bytes: 20 * GI,
1220 gpu: None,
1221 },
1222 InstanceTypeSpec {
1224 name: "Standard_L8s_v3",
1225 platform: Platform::Azure,
1226 family: InstanceFamily::StorageOptimized,
1227 architecture: Architecture::X86_64,
1228 vcpu: 8,
1229 memory_bytes: 64 * GI,
1230 ephemeral_storage_bytes: 1788 * GI,
1231 gpu: None,
1232 },
1233 InstanceTypeSpec {
1234 name: "Standard_L16s_v3",
1235 platform: Platform::Azure,
1236 family: InstanceFamily::StorageOptimized,
1237 architecture: Architecture::X86_64,
1238 vcpu: 16,
1239 memory_bytes: 128 * GI,
1240 ephemeral_storage_bytes: 3576 * GI,
1241 gpu: None,
1242 },
1243 InstanceTypeSpec {
1244 name: "Standard_L32s_v3",
1245 platform: Platform::Azure,
1246 family: InstanceFamily::StorageOptimized,
1247 architecture: Architecture::X86_64,
1248 vcpu: 32,
1249 memory_bytes: 256 * GI,
1250 ephemeral_storage_bytes: 7154 * GI,
1251 gpu: None,
1252 },
1253 InstanceTypeSpec {
1255 name: "Standard_NC4as_T4_v3",
1256 platform: Platform::Azure,
1257 family: InstanceFamily::GpuCompute,
1258 architecture: Architecture::X86_64,
1259 vcpu: 4,
1260 memory_bytes: 28 * GI,
1261 ephemeral_storage_bytes: 176 * GI,
1262 gpu: Some(CatalogGpu {
1263 gpu_type: "nvidia-t4",
1264 count: 1,
1265 }),
1266 },
1267 InstanceTypeSpec {
1269 name: "Standard_NC24ads_A100_v4",
1270 platform: Platform::Azure,
1271 family: InstanceFamily::GpuCompute,
1272 architecture: Architecture::X86_64,
1273 vcpu: 24,
1274 memory_bytes: 220 * GI,
1275 ephemeral_storage_bytes: 958 * GI,
1276 gpu: Some(CatalogGpu {
1277 gpu_type: "nvidia-a100",
1278 count: 1,
1279 }),
1280 },
1281 InstanceTypeSpec {
1282 name: "Standard_NC96ads_A100_v4",
1283 platform: Platform::Azure,
1284 family: InstanceFamily::GpuCompute,
1285 architecture: Architecture::X86_64,
1286 vcpu: 96,
1287 memory_bytes: 880 * GI,
1288 ephemeral_storage_bytes: 3916 * GI,
1289 gpu: Some(CatalogGpu {
1290 gpu_type: "nvidia-a100",
1291 count: 4,
1292 }),
1293 },
1294 InstanceTypeSpec {
1296 name: "Standard_ND96isr_H100_v5",
1297 platform: Platform::Azure,
1298 family: InstanceFamily::GpuCompute,
1299 architecture: Architecture::X86_64,
1300 vcpu: 96,
1301 memory_bytes: 1900 * GI,
1302 ephemeral_storage_bytes: 1000 * GI,
1303 gpu: Some(CatalogGpu {
1304 gpu_type: "nvidia-h100",
1305 count: 8,
1306 }),
1307 },
1308];
1309
1310pub fn catalog_for_platform(platform: Platform) -> Vec<&'static InstanceTypeSpec> {
1316 CATALOG
1317 .iter()
1318 .filter(|spec| spec.platform == platform)
1319 .collect()
1320}
1321
1322pub fn find_instance_type(platform: Platform, name: &str) -> Option<&'static InstanceTypeSpec> {
1324 CATALOG
1325 .iter()
1326 .find(|spec| spec.platform == platform && spec.name == name)
1327}
1328
1329pub fn is_same_architecture_aws_machine(old: &str, new: &str) -> bool {
1332 is_same_architecture_machine(Platform::Aws, old, new)
1333}
1334
1335pub fn is_same_architecture_machine(platform: Platform, old: &str, new: &str) -> bool {
1337 matches!(platform, Platform::Aws | Platform::Gcp | Platform::Azure)
1338 && find_instance_type(platform, old)
1339 .zip(find_instance_type(platform, new))
1340 .is_some_and(|(old, new)| old.architecture == new.architecture)
1341}
1342
1343#[derive(Debug, Clone)]
1349pub struct WorkloadRequirements {
1350 pub total_cpu_at_desired: f64,
1352 pub total_memory_bytes_at_desired: u64,
1354 pub total_cpu_at_max: f64,
1356 pub total_memory_bytes_at_max: u64,
1358 pub max_cpu_per_container: f64,
1360 pub max_memory_per_container: u64,
1362 pub max_ephemeral_storage_bytes: u64,
1364 pub gpu: Option<GpuSpec>,
1366 pub architecture: Option<Architecture>,
1368 pub nested_virt: bool,
1372}
1373
1374#[derive(Debug, Clone)]
1376pub struct InstanceSelection {
1377 pub instance_type: &'static str,
1379 pub profile: MachineProfile,
1381 pub min_machines: u32,
1383 pub max_machines: u32,
1385}
1386
1387const STORAGE_OPTIMIZED_THRESHOLD: u64 = 200 * GI;
1389
1390const MAX_MACHINES_PER_CLUSTER: u32 = 10;
1392
1393const MAX_STANDARD_VCPU: u32 = 8;
1396
1397const SYSTEM_RESERVE_CPU: f64 = 0.5;
1399
1400const WORKLOAD_HEADROOM_FACTOR: f64 = 1.15;
1402
1403pub fn select_instance_type(
1414 platform: Platform,
1415 requirements: &WorkloadRequirements,
1416) -> Result<InstanceSelection, String> {
1417 let family = select_family(requirements);
1418 let family = if requirements.nested_virt && family == InstanceFamily::Burstable {
1419 InstanceFamily::GeneralPurpose
1420 } else {
1421 family
1422 };
1423 let selection = select_instance_type_in_family(platform, requirements, family);
1424 if selection.is_err() && family == InstanceFamily::Burstable {
1425 return select_instance_type_in_family(
1426 platform,
1427 requirements,
1428 InstanceFamily::GeneralPurpose,
1429 );
1430 }
1431 selection
1432}
1433
1434fn select_instance_type_in_family(
1435 platform: Platform,
1436 requirements: &WorkloadRequirements,
1437 family: InstanceFamily,
1438) -> Result<InstanceSelection, String> {
1439 let architecture = requirements
1440 .architecture
1441 .or_else(|| default_architecture(platform))
1442 .ok_or_else(|| format!("platform {platform} has no default compute architecture"))?;
1443
1444 let candidates: Vec<&InstanceTypeSpec> = CATALOG
1445 .iter()
1446 .filter(|spec| spec.platform == platform && spec.family == family)
1447 .filter(|spec| {
1448 if requirements.nested_virt {
1449 spec.is_nested_virt_capable()
1450 } else {
1451 platform != Platform::Aws || !spec.is_nested_virt_capable()
1452 }
1453 })
1454 .filter(|spec| spec.architecture == architecture)
1455 .collect();
1456
1457 if candidates.is_empty() && family != InstanceFamily::StorageOptimized {
1462 let family_has_other_architecture = CATALOG.iter().any(|spec| {
1463 spec.platform == platform
1464 && spec.family == family
1465 && if requirements.nested_virt {
1466 spec.is_nested_virt_capable()
1467 } else {
1468 platform != Platform::Aws || !spec.is_nested_virt_capable()
1469 }
1470 });
1471 if family_has_other_architecture {
1472 return Err(format!(
1473 "architecture {architecture:?} is unavailable for this workload on platform {platform}"
1474 ));
1475 }
1476 return Err(if requirements.nested_virt {
1477 format!(
1478 "no nested-virt-capable {family:?} instance types in catalog for platform {platform}"
1479 )
1480 } else {
1481 format!("no {family:?} instance types in catalog for platform {platform}")
1482 });
1483 }
1484
1485 let candidates = if let Some(ref gpu) = requirements.gpu {
1487 let filtered: Vec<&InstanceTypeSpec> = candidates
1488 .into_iter()
1489 .filter(|spec| {
1490 spec.gpu.as_ref().map_or(false, |g| {
1491 g.gpu_type == gpu.gpu_type && g.count >= gpu.count
1492 })
1493 })
1494 .collect();
1495 if filtered.is_empty() {
1496 return Err(format!(
1497 "no instance type for GPU type '{}' x{} on platform {platform}",
1498 gpu.gpu_type, gpu.count
1499 ));
1500 }
1501 filtered
1502 } else {
1503 candidates
1504 };
1505
1506 let candidates = if family == InstanceFamily::StorageOptimized {
1508 let filtered: Vec<&InstanceTypeSpec> = candidates
1509 .into_iter()
1510 .filter(|spec| spec.ephemeral_storage_bytes >= requirements.max_ephemeral_storage_bytes)
1511 .collect();
1512 if filtered.is_empty() {
1513 CATALOG
1517 .iter()
1518 .filter(|spec| {
1519 spec.platform == platform
1520 && spec.family == InstanceFamily::GeneralPurpose
1521 && spec.has_configurable_ephemeral_storage()
1522 && max_configurable_ephemeral_storage_bytes(platform)
1523 .is_some_and(|max| requirements.max_ephemeral_storage_bytes <= max)
1524 })
1525 .filter(|spec| {
1526 if requirements.nested_virt {
1527 spec.is_nested_virt_capable()
1528 } else {
1529 platform != Platform::Aws || !spec.is_nested_virt_capable()
1530 }
1531 })
1532 .filter(|spec| spec.architecture == architecture)
1533 .collect()
1534 } else {
1535 filtered
1536 }
1537 } else {
1538 candidates
1539 };
1540
1541 if candidates.is_empty() {
1542 return Err(format!(
1543 "architecture {architecture:?} is unavailable for this workload on platform {platform}"
1544 ));
1545 }
1546
1547 let candidates: Vec<_> = candidates
1550 .into_iter()
1551 .filter(|spec| {
1552 allocatable_cpu(spec) >= requirements.max_cpu_per_container * WORKLOAD_HEADROOM_FACTOR
1553 && allocatable_memory_bytes(spec) as f64
1554 >= requirements.max_memory_per_container as f64 * WORKLOAD_HEADROOM_FACTOR
1555 })
1556 .collect();
1557 let smallest_replica_machine = candidates.iter().map(|spec| spec.vcpu).min()
1558 .ok_or_else(|| format!("no {platform} machine can fit one replica with system reserve and workload headroom"))?;
1559
1560 let effective_family = candidates[0].family;
1565 let vcpu_cap = if effective_family == InstanceFamily::GpuCompute
1566 || effective_family == InstanceFamily::StorageOptimized
1567 {
1568 u32::MAX
1569 } else {
1570 MAX_STANDARD_VCPU.max(smallest_replica_machine)
1571 };
1572
1573 let desired_target_machines = desired_target_machines(requirements);
1574 let target_cpu = requirements
1575 .max_cpu_per_container
1576 .max(requirements.total_cpu_at_desired / desired_target_machines as f64)
1577 * WORKLOAD_HEADROOM_FACTOR;
1578 let target_memory = (requirements.max_memory_per_container as f64)
1579 .max(requirements.total_memory_bytes_at_desired as f64 / desired_target_machines as f64)
1580 * WORKLOAD_HEADROOM_FACTOR;
1581
1582 let selected = candidates
1587 .iter()
1588 .filter(|spec| {
1589 spec.vcpu <= vcpu_cap
1590 && allocatable_cpu(spec) >= target_cpu
1591 && allocatable_memory_bytes(spec) as f64 >= target_memory
1592 })
1593 .min_by_key(|spec| spec.vcpu)
1594 .or_else(|| {
1595 candidates
1597 .iter()
1598 .filter(|spec| spec.vcpu <= vcpu_cap)
1599 .max_by_key(|spec| spec.vcpu)
1600 })
1601 .or_else(|| {
1602 candidates.iter().min_by_key(|spec| spec.vcpu)
1604 })
1605 .ok_or_else(|| format!("no instance types available for platform {platform}"))?;
1606
1607 let max_machines = compute_max_machines(requirements, selected);
1609 let min_machines = compute_min_machines(requirements, selected, max_machines);
1610
1611 Ok(InstanceSelection {
1612 instance_type: selected.name,
1613 profile: selected.to_machine_profile_for_storage(requirements.max_ephemeral_storage_bytes),
1614 min_machines,
1615 max_machines,
1616 })
1617}
1618
1619pub fn select_family(requirements: &WorkloadRequirements) -> InstanceFamily {
1625 if requirements.gpu.is_some() {
1627 return InstanceFamily::GpuCompute;
1628 }
1629
1630 if requirements.max_ephemeral_storage_bytes > STORAGE_OPTIMIZED_THRESHOLD {
1632 return InstanceFamily::StorageOptimized;
1633 }
1634
1635 if requirements.total_cpu_at_max < 2.0 {
1637 return InstanceFamily::Burstable;
1638 }
1639
1640 InstanceFamily::GeneralPurpose
1642}
1643
1644fn compute_max_machines(requirements: &WorkloadRequirements, instance: &InstanceTypeSpec) -> u32 {
1646 let cpu_with_headroom = requirements.total_cpu_at_max * WORKLOAD_HEADROOM_FACTOR;
1647 let cpu_machines = (cpu_with_headroom / allocatable_cpu(instance)).ceil() as u32;
1648
1649 let mem_with_headroom =
1650 requirements.total_memory_bytes_at_max as f64 * WORKLOAD_HEADROOM_FACTOR;
1651 let mem_machines =
1652 (mem_with_headroom / allocatable_memory_bytes(instance) as f64).ceil() as u32;
1653
1654 cpu_machines
1656 .max(mem_machines)
1657 .max(1)
1658 .min(MAX_MACHINES_PER_CLUSTER)
1659}
1660
1661fn compute_min_machines(
1663 requirements: &WorkloadRequirements,
1664 instance: &InstanceTypeSpec,
1665 max_machines: u32,
1666) -> u32 {
1667 let cpu_with_headroom = requirements.total_cpu_at_desired * WORKLOAD_HEADROOM_FACTOR;
1668 let cpu_machines = (cpu_with_headroom / allocatable_cpu(instance)).ceil() as u32;
1669
1670 let mem_with_headroom =
1671 requirements.total_memory_bytes_at_desired as f64 * WORKLOAD_HEADROOM_FACTOR;
1672 let mem_machines =
1673 (mem_with_headroom / allocatable_memory_bytes(instance) as f64).ceil() as u32;
1674
1675 cpu_machines
1676 .max(mem_machines)
1677 .max(1)
1678 .min(2)
1679 .min(max_machines)
1680}
1681
1682fn desired_target_machines(requirements: &WorkloadRequirements) -> u32 {
1683 if requirements.total_cpu_at_desired >= 2.0
1684 || requirements.total_memory_bytes_at_desired >= 4 * GI
1685 {
1686 2
1687 } else {
1688 1
1689 }
1690}
1691
1692pub(crate) fn allocatable_cpu(instance: &InstanceTypeSpec) -> f64 {
1693 (instance.vcpu as f64 - SYSTEM_RESERVE_CPU).max(0.25)
1694}
1695
1696pub(crate) fn allocatable_memory_bytes(instance: &InstanceTypeSpec) -> u64 {
1697 instance
1698 .memory_bytes
1699 .saturating_sub(system_reserve_memory_bytes(instance.memory_bytes))
1700 .max(256 * MI)
1701}
1702
1703fn system_reserve_memory_bytes(memory_bytes: u64) -> u64 {
1704 if memory_bytes < 4 * GI {
1705 256 * MI
1706 } else if memory_bytes < 16 * GI {
1707 512 * MI
1708 } else {
1709 GI
1710 }
1711}
1712
1713#[cfg(test)]
1718mod tests {
1719 use super::*;
1720 use crate::BinaryTarget;
1721
1722 #[test]
1725 fn test_parse_cpu_plain() {
1726 assert_eq!(parse_cpu("1").unwrap(), 1.0);
1727 assert_eq!(parse_cpu("0.5").unwrap(), 0.5);
1728 assert_eq!(parse_cpu("2.0").unwrap(), 2.0);
1729 assert_eq!(parse_cpu("16").unwrap(), 16.0);
1730 }
1731
1732 #[test]
1733 fn test_parse_cpu_millicore() {
1734 assert_eq!(parse_cpu("500m").unwrap(), 0.5);
1735 assert_eq!(parse_cpu("250m").unwrap(), 0.25);
1736 assert_eq!(parse_cpu("1000m").unwrap(), 1.0);
1737 assert_eq!(parse_cpu("100m").unwrap(), 0.1);
1738 }
1739
1740 #[test]
1741 fn test_parse_cpu_invalid() {
1742 assert!(parse_cpu("").is_err());
1743 assert!(parse_cpu("abc").is_err());
1744 assert!(parse_cpu("m").is_err());
1745 }
1746
1747 #[test]
1748 fn test_parse_memory_binary_suffixes() {
1749 assert_eq!(parse_memory_bytes("1Ki").unwrap(), 1024);
1750 assert_eq!(parse_memory_bytes("1Mi").unwrap(), 1024 * 1024);
1751 assert_eq!(parse_memory_bytes("1Gi").unwrap(), 1024 * 1024 * 1024);
1752 assert_eq!(parse_memory_bytes("4Gi").unwrap(), 4 * 1024 * 1024 * 1024);
1753 assert_eq!(parse_memory_bytes("512Mi").unwrap(), 512 * 1024 * 1024);
1754 assert_eq!(
1755 parse_memory_bytes("1Ti").unwrap(),
1756 1024u64 * 1024 * 1024 * 1024
1757 );
1758 }
1759
1760 #[test]
1761 fn test_parse_memory_decimal_suffixes() {
1762 assert_eq!(parse_memory_bytes("1k").unwrap(), 1000);
1763 assert_eq!(parse_memory_bytes("1M").unwrap(), 1_000_000);
1764 assert_eq!(parse_memory_bytes("1G").unwrap(), 1_000_000_000);
1765 assert_eq!(parse_memory_bytes("1T").unwrap(), 1_000_000_000_000);
1766 }
1767
1768 #[test]
1769 fn test_parse_memory_plain_bytes() {
1770 assert_eq!(parse_memory_bytes("1024").unwrap(), 1024);
1771 assert_eq!(parse_memory_bytes("0").unwrap(), 0);
1772 }
1773
1774 #[test]
1775 fn test_parse_memory_invalid() {
1776 assert!(parse_memory_bytes("").is_err());
1777 assert!(parse_memory_bytes("abc").is_err());
1778 assert!(parse_memory_bytes("Gi").is_err());
1779 }
1780
1781 #[test]
1782 fn test_parse_memory_fractional() {
1783 assert_eq!(parse_memory_bytes("0.5Gi").unwrap(), GI / 2);
1784 assert_eq!(parse_memory_bytes("1.5Gi").unwrap(), GI + GI / 2);
1785 }
1786
1787 #[test]
1790 fn test_catalog_has_entries_for_all_cloud_platforms() {
1791 assert!(!catalog_for_platform(Platform::Aws).is_empty());
1792 assert!(!catalog_for_platform(Platform::Gcp).is_empty());
1793 assert!(!catalog_for_platform(Platform::Azure).is_empty());
1794 }
1795
1796 #[test]
1797 fn test_catalog_no_entries_for_non_cloud_platforms() {
1798 assert!(catalog_for_platform(Platform::Local).is_empty());
1799 assert!(catalog_for_platform(Platform::Kubernetes).is_empty());
1800 }
1801
1802 #[test]
1803 fn test_find_known_instance_type() {
1804 let spec =
1805 find_instance_type(Platform::Aws, "m7g.2xlarge").expect("should find m7g.2xlarge");
1806 assert_eq!(spec.vcpu, 8);
1807 assert_eq!(spec.memory_bytes, 32 * GI);
1808 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1809 }
1810
1811 #[test]
1812 fn test_find_aws_c8i_nested_virt_instance_type() {
1813 let spec = find_instance_type(Platform::Aws, "c8i.large").expect("should find c8i.large");
1814 assert_eq!(spec.vcpu, 2);
1815 assert_eq!(spec.memory_bytes, 4 * GI);
1816 assert_eq!(spec.family, InstanceFamily::ComputeOptimized);
1817 assert_eq!(spec.architecture, Architecture::X86_64);
1818 assert!(spec.is_nested_virt_capable());
1819 }
1820
1821 #[test]
1822 fn test_find_unknown_instance_type() {
1823 assert!(find_instance_type(Platform::Aws, "nonexistent.xlarge").is_none());
1824 }
1825
1826 #[test]
1827 fn test_find_wrong_platform() {
1828 assert!(find_instance_type(Platform::Gcp, "m7g.2xlarge").is_none());
1829 }
1830
1831 #[test]
1832 fn test_to_machine_profile() {
1833 let spec = find_instance_type(Platform::Aws, "m7g.2xlarge").unwrap();
1834 let profile = spec.to_machine_profile();
1835 assert_eq!(profile.cpu, "8.0");
1836 assert_eq!(profile.memory_bytes, 32 * GI);
1837 assert_eq!(profile.ephemeral_storage_bytes, 20 * GI);
1838 assert!(profile.gpu.is_none());
1839 }
1840
1841 #[test]
1842 fn test_to_machine_profile_with_gpu() {
1843 let spec = find_instance_type(Platform::Aws, "p4d.24xlarge").unwrap();
1844 let profile = spec.to_machine_profile();
1845 let gpu = profile.gpu.as_ref().expect("should have GPU");
1846 assert_eq!(gpu.gpu_type, "nvidia-a100");
1847 assert_eq!(gpu.count, 8);
1848 }
1849
1850 #[test]
1853 fn small_cpu_with_large_memory_falls_back_from_burstable() {
1854 let requirements = WorkloadRequirements {
1855 total_cpu_at_desired: 0.5,
1856 total_memory_bytes_at_desired: 8 * GI,
1857 total_cpu_at_max: 0.5,
1858 total_memory_bytes_at_max: 8 * GI,
1859 max_cpu_per_container: 0.5,
1860 max_memory_per_container: 8 * GI,
1861 max_ephemeral_storage_bytes: 0,
1862 gpu: None,
1863 architecture: Some(Architecture::X86_64),
1864 nested_virt: false,
1865 };
1866 let selected = select_instance_type(Platform::Gcp, &requirements).unwrap();
1867 let machine = find_instance_type(Platform::Gcp, selected.instance_type).unwrap();
1868 assert!(
1869 allocatable_memory_bytes(machine) as f64 >= 8.0 * GI as f64 * WORKLOAD_HEADROOM_FACTOR
1870 );
1871 }
1872
1873 #[test]
1874 fn test_select_burstable_for_small_workload() {
1875 let req = WorkloadRequirements {
1876 total_cpu_at_desired: 1.0,
1877 total_memory_bytes_at_desired: 2 * GI,
1878 total_cpu_at_max: 1.0,
1879 total_memory_bytes_at_max: 2 * GI,
1880 max_cpu_per_container: 0.5,
1881 max_memory_per_container: 1 * GI,
1882 max_ephemeral_storage_bytes: 10 * GI,
1883 gpu: None,
1884 architecture: None,
1885 nested_virt: false,
1886 };
1887 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1888 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1889 assert_eq!(spec.family, InstanceFamily::Burstable);
1890 }
1891
1892 #[test]
1893 fn test_selects_smallest_burstable_machine_with_real_headroom() {
1894 let req = WorkloadRequirements {
1895 total_cpu_at_desired: 1.0,
1896 total_memory_bytes_at_desired: 2 * GI,
1897 total_cpu_at_max: 1.0,
1898 total_memory_bytes_at_max: 2 * GI,
1899 max_cpu_per_container: 1.0,
1900 max_memory_per_container: 2 * GI,
1901 max_ephemeral_storage_bytes: 10 * GI,
1902 gpu: None,
1903 architecture: None,
1904 nested_virt: false,
1905 };
1906
1907 let selection = select_instance_type(Platform::Aws, &req).unwrap();
1908
1909 assert_eq!(selection.instance_type, "t4g.medium");
1910 assert_eq!(selection.min_machines, 1);
1911 assert_eq!(selection.max_machines, 1);
1912 }
1913
1914 #[test]
1915 fn test_select_general_purpose_for_standard_workload() {
1916 let req = WorkloadRequirements {
1918 total_cpu_at_desired: 20.0,
1919 total_memory_bytes_at_desired: 80 * GI,
1920 total_cpu_at_max: 20.0,
1921 total_memory_bytes_at_max: 80 * GI,
1922 max_cpu_per_container: 2.0,
1923 max_memory_per_container: 8 * GI,
1924 max_ephemeral_storage_bytes: 10 * GI,
1925 gpu: None,
1926 architecture: None,
1927 nested_virt: false,
1928 };
1929 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1930 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1931 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1932 }
1933
1934 #[test]
1935 fn test_select_general_purpose_even_for_cpu_heavy() {
1936 let req = WorkloadRequirements {
1938 total_cpu_at_desired: 20.0,
1939 total_memory_bytes_at_desired: 20 * GI,
1940 total_cpu_at_max: 20.0,
1941 total_memory_bytes_at_max: 20 * GI,
1942 max_cpu_per_container: 2.0,
1943 max_memory_per_container: 2 * GI,
1944 max_ephemeral_storage_bytes: 10 * GI,
1945 gpu: None,
1946 architecture: None,
1947 nested_virt: false,
1948 };
1949 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1950 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1951 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1952 }
1953
1954 #[test]
1955 fn test_select_storage_optimized_for_large_ephemeral() {
1956 let req = WorkloadRequirements {
1957 total_cpu_at_desired: 8.0,
1958 total_memory_bytes_at_desired: 32 * GI,
1959 total_cpu_at_max: 8.0,
1960 total_memory_bytes_at_max: 32 * GI,
1961 max_cpu_per_container: 2.0,
1962 max_memory_per_container: 8 * GI,
1963 max_ephemeral_storage_bytes: 500 * GI,
1964 gpu: None,
1965 architecture: Some(Architecture::X86_64),
1966 nested_virt: false,
1967 };
1968 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1969 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1970 assert_eq!(spec.family, InstanceFamily::StorageOptimized);
1971 }
1972
1973 #[test]
1974 fn test_select_configurable_storage_above_fixed_local_catalog() {
1975 let req = WorkloadRequirements {
1976 total_cpu_at_desired: 8.0,
1977 total_memory_bytes_at_desired: 32 * GI,
1978 total_cpu_at_max: 8.0,
1979 total_memory_bytes_at_max: 32 * GI,
1980 max_cpu_per_container: 2.0,
1981 max_memory_per_container: 8 * GI,
1982 max_ephemeral_storage_bytes: 8_000 * GI,
1983 gpu: None,
1984 architecture: Some(Architecture::X86_64),
1985 nested_virt: false,
1986 };
1987 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1988 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1989 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1990 assert_eq!(sel.profile.ephemeral_storage_bytes, 8_000 * GI);
1991 }
1992
1993 #[test]
1994 fn test_configurable_storage_fallback_retains_standard_vcpu_cap() {
1995 let req = WorkloadRequirements {
1996 total_cpu_at_desired: 70.0,
1997 total_memory_bytes_at_desired: 140 * GI,
1998 total_cpu_at_max: 70.0,
1999 total_memory_bytes_at_max: 140 * GI,
2000 max_cpu_per_container: 2.0,
2001 max_memory_per_container: 4 * GI,
2002 max_ephemeral_storage_bytes: 8_000 * GI,
2003 gpu: None,
2004 architecture: Some(Architecture::X86_64),
2005 nested_virt: false,
2006 };
2007
2008 let sel = select_instance_type(Platform::Aws, &req).unwrap();
2009 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2010 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
2011 assert!(spec.vcpu <= MAX_STANDARD_VCPU);
2012 assert!(sel.max_machines > 1);
2013 }
2014
2015 #[test]
2016 fn test_nested_virtualization_storage_falls_back_to_configurable_disk() {
2017 for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
2018 let req = WorkloadRequirements {
2019 total_cpu_at_desired: 2.0,
2020 total_memory_bytes_at_desired: 8 * GI,
2021 total_cpu_at_max: 2.0,
2022 total_memory_bytes_at_max: 8 * GI,
2023 max_cpu_per_container: 2.0,
2024 max_memory_per_container: 8 * GI,
2025 max_ephemeral_storage_bytes: 500 * GI,
2026 gpu: None,
2027 architecture: Some(Architecture::X86_64),
2028 nested_virt: true,
2029 };
2030
2031 let sel = select_instance_type(platform, &req)
2032 .unwrap_or_else(|error| panic!("{platform} should support fallback: {error}"));
2033 let spec = find_instance_type(platform, sel.instance_type).unwrap();
2034 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
2035 assert!(spec.is_nested_virt_capable());
2036 assert_eq!(sel.profile.ephemeral_storage_bytes, 500 * GI);
2037 }
2038 }
2039
2040 #[test]
2041 fn test_configurable_disk_rejects_capacity_above_provider_limit() {
2042 for (platform, requested) in [
2043 (Platform::Aws, 100 * TI),
2044 (Platform::Gcp, 100 * TI),
2045 (Platform::Azure, 8_000 * GI),
2046 ] {
2047 let req = WorkloadRequirements {
2048 total_cpu_at_desired: 2.0,
2049 total_memory_bytes_at_desired: 8 * GI,
2050 total_cpu_at_max: 2.0,
2051 total_memory_bytes_at_max: 8 * GI,
2052 max_cpu_per_container: 2.0,
2053 max_memory_per_container: 8 * GI,
2054 max_ephemeral_storage_bytes: requested,
2055 gpu: None,
2056 architecture: Some(Architecture::X86_64),
2057 nested_virt: true,
2058 };
2059
2060 assert!(select_instance_type(platform, &req).is_err());
2061 }
2062 }
2063
2064 #[test]
2065 fn test_configurable_disk_limits_account_for_controller_rounding() {
2066 for (platform, disk_limit_gib) in [
2067 (Platform::Aws, 64 * 1024),
2068 (Platform::Gcp, 64 * 1024),
2069 (Platform::Azure, 4_095),
2070 ] {
2071 let max = max_configurable_ephemeral_storage_bytes(platform).unwrap();
2072 let materialized_disk_gib = |requested_bytes: u64| {
2073 let requested_gib = requested_bytes.div_ceil(GI);
2074 (requested_gib * 5).div_ceil(4) + 12
2075 };
2076
2077 assert!(materialized_disk_gib(max) <= disk_limit_gib);
2078 assert!(materialized_disk_gib(max + 1) > disk_limit_gib);
2079 }
2080 }
2081
2082 #[test]
2083 fn test_gpu_local_storage_is_not_treated_as_resizable() {
2084 let spec = find_instance_type(Platform::Aws, "g5.xlarge").unwrap();
2085 assert_eq!(spec.family, InstanceFamily::GpuCompute);
2086 assert!(!spec.has_configurable_ephemeral_storage());
2087 }
2088
2089 #[test]
2090 fn test_select_gpu_instance() {
2091 let req = WorkloadRequirements {
2092 total_cpu_at_desired: 8.0,
2093 total_memory_bytes_at_desired: 32 * GI,
2094 total_cpu_at_max: 8.0,
2095 total_memory_bytes_at_max: 32 * GI,
2096 max_cpu_per_container: 4.0,
2097 max_memory_per_container: 16 * GI,
2098 max_ephemeral_storage_bytes: 10 * GI,
2099 gpu: Some(GpuSpec {
2100 gpu_type: "nvidia-a100".to_string(),
2101 count: 1,
2102 }),
2103 architecture: Some(Architecture::X86_64),
2104 nested_virt: false,
2105 };
2106 let sel = select_instance_type(Platform::Aws, &req).unwrap();
2107 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2108 assert_eq!(spec.family, InstanceFamily::GpuCompute);
2109 assert!(spec.gpu.is_some());
2110 }
2111
2112 #[test]
2113 fn test_select_uses_each_cloud_image_target_architecture() {
2114 let req = WorkloadRequirements {
2115 total_cpu_at_desired: 4.0,
2116 total_memory_bytes_at_desired: 16 * GI,
2117 total_cpu_at_max: 4.0,
2118 total_memory_bytes_at_max: 16 * GI,
2119 max_cpu_per_container: 1.0,
2120 max_memory_per_container: 4 * GI,
2121 max_ephemeral_storage_bytes: 10 * GI,
2122 gpu: None,
2123 architecture: None,
2124 nested_virt: false,
2125 };
2126 for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
2127 let sel = select_instance_type(platform, &req)
2128 .unwrap_or_else(|error| panic!("selection failed for {platform}: {error}"));
2129 let spec = find_instance_type(platform, sel.instance_type)
2130 .expect("selected machine should exist in the catalog");
2131 assert_eq!(
2132 Some(spec.architecture),
2133 default_architecture(platform),
2134 "machine architecture must match the image target for {platform}"
2135 );
2136 }
2137 }
2138
2139 #[test]
2140 fn test_machine_count_reasonable() {
2141 let req = WorkloadRequirements {
2143 total_cpu_at_desired: 20.0,
2144 total_memory_bytes_at_desired: 40 * GI,
2145 total_cpu_at_max: 20.0,
2146 total_memory_bytes_at_max: 40 * GI,
2147 max_cpu_per_container: 1.0,
2148 max_memory_per_container: 2 * GI,
2149 max_ephemeral_storage_bytes: 10 * GI,
2150 gpu: None,
2151 architecture: None,
2152 nested_virt: false,
2153 };
2154 let sel = select_instance_type(Platform::Aws, &req).unwrap();
2155 assert!(sel.min_machines >= 1);
2156 assert!(sel.max_machines <= MAX_MACHINES_PER_CLUSTER);
2157 assert!(sel.max_machines >= sel.min_machines);
2158 }
2159
2160 #[test]
2161 fn test_instance_size_capped_at_8_vcpu() {
2162 let req = WorkloadRequirements {
2164 total_cpu_at_desired: 70.0,
2165 total_memory_bytes_at_desired: 140 * GI,
2166 total_cpu_at_max: 70.0,
2167 total_memory_bytes_at_max: 140 * GI,
2168 max_cpu_per_container: 2.0,
2169 max_memory_per_container: 4 * GI,
2170 max_ephemeral_storage_bytes: 10 * GI,
2171 gpu: None,
2172 architecture: None,
2173 nested_virt: false,
2174 };
2175 let sel = select_instance_type(Platform::Gcp, &req).unwrap();
2176 let spec = find_instance_type(Platform::Gcp, sel.instance_type).unwrap();
2177 assert!(
2178 spec.vcpu <= MAX_STANDARD_VCPU,
2179 "selected {} with {} vCPUs, expected <= {}",
2180 spec.name,
2181 spec.vcpu,
2182 MAX_STANDARD_VCPU
2183 );
2184 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
2185 assert!(sel.max_machines > 1);
2187 }
2188
2189 #[test]
2190 fn test_larger_autoscaled_workload_gets_reasonable_instance() {
2191 let req = WorkloadRequirements {
2194 total_cpu_at_desired: 70.0,
2195 total_memory_bytes_at_desired: 140 * GI,
2196 total_cpu_at_max: 70.0, total_memory_bytes_at_max: 140 * GI, max_cpu_per_container: 2.0,
2199 max_memory_per_container: 4 * GI,
2200 max_ephemeral_storage_bytes: 20 * GI,
2201 gpu: None,
2202 architecture: None,
2203 nested_virt: false,
2204 };
2205 let sel = select_instance_type(Platform::Gcp, &req).unwrap();
2206 assert_eq!(sel.instance_type, "n2-standard-8");
2208 assert!(sel.max_machines >= 2);
2209 }
2210
2211 #[test]
2218 fn test_select_aws_picks_m8i_when_nested_virt_required() {
2219 let req = WorkloadRequirements {
2220 total_cpu_at_desired: 4.0,
2221 total_memory_bytes_at_desired: 8 * GI,
2222 total_cpu_at_max: 4.0,
2223 total_memory_bytes_at_max: 8 * GI,
2224 max_cpu_per_container: 4.0,
2225 max_memory_per_container: 8 * GI,
2226 max_ephemeral_storage_bytes: 10 * GI,
2227 gpu: None,
2228 architecture: Some(Architecture::X86_64),
2229 nested_virt: true,
2230 };
2231 let sel = select_instance_type(Platform::Aws, &req).unwrap();
2232 assert!(
2233 sel.instance_type.starts_with("m8i.")
2234 || sel.instance_type.starts_with("c8i.")
2235 || sel.instance_type.starts_with("r8i."),
2236 "expected an m8i/c8i/r8i instance, got {}",
2237 sel.instance_type
2238 );
2239 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2240 assert!(spec.is_nested_virt_capable());
2241 }
2242
2243 #[test]
2244 fn test_select_gcp_picks_n2_when_nested_virt_required() {
2245 let req = WorkloadRequirements {
2246 total_cpu_at_desired: 4.0,
2247 total_memory_bytes_at_desired: 8 * GI,
2248 total_cpu_at_max: 4.0,
2249 total_memory_bytes_at_max: 8 * GI,
2250 max_cpu_per_container: 4.0,
2251 max_memory_per_container: 8 * GI,
2252 max_ephemeral_storage_bytes: 0,
2253 architecture: Some(Architecture::X86_64),
2254 gpu: None,
2255 nested_virt: true,
2256 };
2257
2258 let selection = select_instance_type(Platform::Gcp, &req).unwrap();
2259 assert_eq!(selection.instance_type, "n2-standard-8");
2260 assert!(find_instance_type(Platform::Gcp, selection.instance_type)
2261 .unwrap()
2262 .is_nested_virt_capable());
2263 }
2264
2265 #[test]
2266 fn test_select_azure_picks_dsv5_when_nested_virt_required() {
2267 let req = WorkloadRequirements {
2268 total_cpu_at_desired: 4.0,
2269 total_memory_bytes_at_desired: 8 * GI,
2270 total_cpu_at_max: 4.0,
2271 total_memory_bytes_at_max: 8 * GI,
2272 max_cpu_per_container: 4.0,
2273 max_memory_per_container: 8 * GI,
2274 max_ephemeral_storage_bytes: 0,
2275 architecture: Some(Architecture::X86_64),
2276 gpu: None,
2277 nested_virt: true,
2278 };
2279
2280 let selection = select_instance_type(Platform::Azure, &req).unwrap();
2281 assert_eq!(selection.instance_type, "Standard_D8s_v5");
2282 assert!(find_instance_type(Platform::Azure, selection.instance_type)
2283 .unwrap()
2284 .is_nested_virt_capable());
2285 }
2286
2287 #[test]
2288 fn test_select_aws_defaults_to_image_target_architecture() {
2289 let req = WorkloadRequirements {
2290 total_cpu_at_desired: 4.0,
2291 total_memory_bytes_at_desired: 8 * GI,
2292 total_cpu_at_max: 4.0,
2293 total_memory_bytes_at_max: 8 * GI,
2294 max_cpu_per_container: 4.0,
2295 max_memory_per_container: 8 * GI,
2296 max_ephemeral_storage_bytes: 10 * GI,
2297 gpu: None,
2298 architecture: None,
2299 nested_virt: false,
2300 };
2301 let sel = select_instance_type(Platform::Aws, &req).unwrap();
2302 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2303 assert_eq!(spec.architecture, Architecture::Arm64);
2304 }
2305
2306 #[test]
2307 fn test_cloud_defaults_match_image_target_architectures() {
2308 for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
2309 let target = BinaryTarget::defaults_for_platform(platform)
2310 .into_iter()
2311 .next()
2312 .expect("managed cloud should have a default image target");
2313 let image_architecture = match target.oci_arch() {
2314 "arm64" => Architecture::Arm64,
2315 "amd64" => Architecture::X86_64,
2316 architecture => {
2317 panic!("unsupported managed-cloud image architecture {architecture}")
2318 }
2319 };
2320
2321 assert_eq!(default_architecture(platform), Some(image_architecture));
2322 }
2323 }
2324
2325 #[test]
2327 fn test_select_aws_uses_graviton_for_explicit_arm64() {
2328 let req = WorkloadRequirements {
2329 total_cpu_at_desired: 4.0,
2330 total_memory_bytes_at_desired: 8 * GI,
2331 total_cpu_at_max: 4.0,
2332 total_memory_bytes_at_max: 8 * GI,
2333 max_cpu_per_container: 4.0,
2334 max_memory_per_container: 8 * GI,
2335 max_ephemeral_storage_bytes: 10 * GI,
2336 gpu: None,
2337 architecture: Some(Architecture::Arm64),
2338 nested_virt: false,
2339 };
2340 let sel = select_instance_type(Platform::Aws, &req).unwrap();
2341 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2342 assert_eq!(spec.architecture, Architecture::Arm64);
2343 }
2344
2345 #[test]
2346 fn test_select_rejects_explicit_architecture_missing_from_cloud_catalog() {
2347 let req = WorkloadRequirements {
2348 total_cpu_at_desired: 1.0,
2349 total_memory_bytes_at_desired: 2 * GI,
2350 total_cpu_at_max: 1.0,
2351 total_memory_bytes_at_max: 2 * GI,
2352 max_cpu_per_container: 1.0,
2353 max_memory_per_container: 2 * GI,
2354 max_ephemeral_storage_bytes: 10 * GI,
2355 gpu: None,
2356 architecture: Some(Architecture::Arm64),
2357 nested_virt: false,
2358 };
2359
2360 let error = select_instance_type(Platform::Gcp, &req)
2361 .expect_err("GCP catalog has no ARM64 machine");
2362
2363 assert!(error.contains("architecture Arm64 is unavailable"));
2364 }
2365
2366 #[test]
2367 fn test_profile_has_required_fields() {
2368 let req = WorkloadRequirements {
2369 total_cpu_at_desired: 4.0,
2370 total_memory_bytes_at_desired: 16 * GI,
2371 total_cpu_at_max: 4.0,
2372 total_memory_bytes_at_max: 16 * GI,
2373 max_cpu_per_container: 1.0,
2374 max_memory_per_container: 4 * GI,
2375 max_ephemeral_storage_bytes: 10 * GI,
2376 gpu: None,
2377 architecture: None,
2378 nested_virt: false,
2379 };
2380 let sel = select_instance_type(Platform::Aws, &req).unwrap();
2381 assert!(!sel.profile.cpu.is_empty());
2382 assert!(sel.profile.memory_bytes > 0);
2383 assert!(sel.profile.ephemeral_storage_bytes > 0);
2384 }
2385
2386 #[test]
2387 fn test_error_for_unsupported_gpu_type() {
2388 let req = WorkloadRequirements {
2389 total_cpu_at_desired: 8.0,
2390 total_memory_bytes_at_desired: 32 * GI,
2391 total_cpu_at_max: 8.0,
2392 total_memory_bytes_at_max: 32 * GI,
2393 max_cpu_per_container: 4.0,
2394 max_memory_per_container: 16 * GI,
2395 max_ephemeral_storage_bytes: 10 * GI,
2396 gpu: Some(GpuSpec {
2397 gpu_type: "amd-mi300".to_string(),
2398 count: 1,
2399 }),
2400 architecture: None,
2401 nested_virt: false,
2402 };
2403 let result = select_instance_type(Platform::Aws, &req);
2404 assert!(result.is_err());
2405 }
2406
2407 #[test]
2408 fn test_catalog_instance_types_sorted_by_vcpu_within_family() {
2409 for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
2412 let entries = catalog_for_platform(platform);
2413 let mut by_family: std::collections::HashMap<_, Vec<_>> =
2414 std::collections::HashMap::new();
2415 for entry in entries {
2416 by_family
2417 .entry(format!("{:?}", entry.family))
2418 .or_default()
2419 .push(entry);
2420 }
2421 for (family, instances) in &by_family {
2422 for window in instances.windows(2) {
2423 assert!(
2424 window[0].vcpu <= window[1].vcpu,
2425 "catalog not sorted by vcpu for {platform}/{family}: {} ({}) > {} ({})",
2426 window[0].name,
2427 window[0].vcpu,
2428 window[1].name,
2429 window[1].vcpu
2430 );
2431 }
2432 }
2433 }
2434 }
2435}