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 if requirements.architecture.is_none()
1422 && (requirements.nested_virt || requirements.gpu.is_some())
1423 {
1424 if let Some(default) = default_architecture(platform) {
1425 let other = match default {
1426 Architecture::Arm64 => Architecture::X86_64,
1427 Architecture::X86_64 => Architecture::Arm64,
1428 };
1429 let with_architecture = |architecture| WorkloadRequirements {
1430 architecture: Some(architecture),
1431 ..requirements.clone()
1432 };
1433 let selection = select_instance_type_for_architecture(
1434 platform,
1435 &with_architecture(default),
1436 );
1437 if selection.is_ok() {
1438 return selection;
1439 }
1440 return select_instance_type_for_architecture(platform, &with_architecture(other))
1443 .map_err(|other_error| {
1444 let default_error = selection.err().unwrap_or_default();
1445 format!("{default:?}: {default_error}; {other:?}: {other_error}")
1446 });
1447 }
1448 }
1449 select_instance_type_for_architecture(platform, requirements)
1450}
1451
1452fn select_instance_type_for_architecture(
1453 platform: Platform,
1454 requirements: &WorkloadRequirements,
1455) -> Result<InstanceSelection, String> {
1456 let family = select_family(requirements);
1457 let family = if requirements.nested_virt && family == InstanceFamily::Burstable {
1458 InstanceFamily::GeneralPurpose
1459 } else {
1460 family
1461 };
1462 let selection = select_instance_type_in_family(platform, requirements, family);
1463 if selection.is_err() && family == InstanceFamily::Burstable {
1464 return select_instance_type_in_family(
1465 platform,
1466 requirements,
1467 InstanceFamily::GeneralPurpose,
1468 );
1469 }
1470 selection
1471}
1472
1473fn select_instance_type_in_family(
1474 platform: Platform,
1475 requirements: &WorkloadRequirements,
1476 family: InstanceFamily,
1477) -> Result<InstanceSelection, String> {
1478 let architecture = requirements
1479 .architecture
1480 .or_else(|| default_architecture(platform))
1481 .ok_or_else(|| format!("platform {platform} has no default compute architecture"))?;
1482
1483 let candidates: Vec<&InstanceTypeSpec> = CATALOG
1484 .iter()
1485 .filter(|spec| spec.platform == platform && spec.family == family)
1486 .filter(|spec| {
1487 if requirements.nested_virt {
1488 spec.is_nested_virt_capable()
1489 } else {
1490 platform != Platform::Aws || !spec.is_nested_virt_capable()
1491 }
1492 })
1493 .filter(|spec| spec.architecture == architecture)
1494 .collect();
1495
1496 if candidates.is_empty() && family != InstanceFamily::StorageOptimized {
1501 let family_has_other_architecture = CATALOG.iter().any(|spec| {
1502 spec.platform == platform
1503 && spec.family == family
1504 && if requirements.nested_virt {
1505 spec.is_nested_virt_capable()
1506 } else {
1507 platform != Platform::Aws || !spec.is_nested_virt_capable()
1508 }
1509 });
1510 if family_has_other_architecture {
1511 return Err(format!(
1512 "architecture {architecture:?} is unavailable for this workload on platform {platform}"
1513 ));
1514 }
1515 return Err(if requirements.nested_virt {
1516 format!(
1517 "no nested-virt-capable {family:?} instance types in catalog for platform {platform}"
1518 )
1519 } else {
1520 format!("no {family:?} instance types in catalog for platform {platform}")
1521 });
1522 }
1523
1524 let candidates = if let Some(ref gpu) = requirements.gpu {
1526 let filtered: Vec<&InstanceTypeSpec> = candidates
1527 .into_iter()
1528 .filter(|spec| {
1529 spec.gpu.as_ref().map_or(false, |g| {
1530 g.gpu_type == gpu.gpu_type && g.count >= gpu.count
1531 })
1532 })
1533 .collect();
1534 if filtered.is_empty() {
1535 return Err(format!(
1536 "no instance type for GPU type '{}' x{} on platform {platform}",
1537 gpu.gpu_type, gpu.count
1538 ));
1539 }
1540 filtered
1541 } else {
1542 candidates
1543 };
1544
1545 let candidates = if family == InstanceFamily::StorageOptimized {
1547 let filtered: Vec<&InstanceTypeSpec> = candidates
1548 .into_iter()
1549 .filter(|spec| spec.ephemeral_storage_bytes >= requirements.max_ephemeral_storage_bytes)
1550 .collect();
1551 if filtered.is_empty() {
1552 CATALOG
1556 .iter()
1557 .filter(|spec| {
1558 spec.platform == platform
1559 && spec.family == InstanceFamily::GeneralPurpose
1560 && spec.has_configurable_ephemeral_storage()
1561 && max_configurable_ephemeral_storage_bytes(platform)
1562 .is_some_and(|max| requirements.max_ephemeral_storage_bytes <= max)
1563 })
1564 .filter(|spec| {
1565 if requirements.nested_virt {
1566 spec.is_nested_virt_capable()
1567 } else {
1568 platform != Platform::Aws || !spec.is_nested_virt_capable()
1569 }
1570 })
1571 .filter(|spec| spec.architecture == architecture)
1572 .collect()
1573 } else {
1574 filtered
1575 }
1576 } else {
1577 candidates
1578 };
1579
1580 if candidates.is_empty() {
1581 return Err(format!(
1582 "architecture {architecture:?} is unavailable for this workload on platform {platform}"
1583 ));
1584 }
1585
1586 let candidates: Vec<_> = candidates
1589 .into_iter()
1590 .filter(|spec| {
1591 allocatable_cpu(spec) >= requirements.max_cpu_per_container * WORKLOAD_HEADROOM_FACTOR
1592 && allocatable_memory_bytes(spec) as f64
1593 >= requirements.max_memory_per_container as f64 * WORKLOAD_HEADROOM_FACTOR
1594 })
1595 .collect();
1596 let smallest_replica_machine = candidates.iter().map(|spec| spec.vcpu).min()
1597 .ok_or_else(|| format!("no {platform} machine can fit one replica with system reserve and workload headroom"))?;
1598
1599 let effective_family = candidates[0].family;
1604 let vcpu_cap = if effective_family == InstanceFamily::GpuCompute
1605 || effective_family == InstanceFamily::StorageOptimized
1606 {
1607 u32::MAX
1608 } else {
1609 MAX_STANDARD_VCPU.max(smallest_replica_machine)
1610 };
1611
1612 let desired_target_machines = desired_target_machines(requirements);
1613 let target_cpu = requirements
1614 .max_cpu_per_container
1615 .max(requirements.total_cpu_at_desired / desired_target_machines as f64)
1616 * WORKLOAD_HEADROOM_FACTOR;
1617 let target_memory = (requirements.max_memory_per_container as f64)
1618 .max(requirements.total_memory_bytes_at_desired as f64 / desired_target_machines as f64)
1619 * WORKLOAD_HEADROOM_FACTOR;
1620
1621 let selected = candidates
1626 .iter()
1627 .filter(|spec| {
1628 spec.vcpu <= vcpu_cap
1629 && allocatable_cpu(spec) >= target_cpu
1630 && allocatable_memory_bytes(spec) as f64 >= target_memory
1631 })
1632 .min_by_key(|spec| spec.vcpu)
1633 .or_else(|| {
1634 candidates
1636 .iter()
1637 .filter(|spec| spec.vcpu <= vcpu_cap)
1638 .max_by_key(|spec| spec.vcpu)
1639 })
1640 .or_else(|| {
1641 candidates.iter().min_by_key(|spec| spec.vcpu)
1643 })
1644 .ok_or_else(|| format!("no instance types available for platform {platform}"))?;
1645
1646 let max_machines = compute_max_machines(requirements, selected);
1648 let min_machines = compute_min_machines(requirements, selected, max_machines);
1649
1650 Ok(InstanceSelection {
1651 instance_type: selected.name,
1652 profile: selected.to_machine_profile_for_storage(requirements.max_ephemeral_storage_bytes),
1653 min_machines,
1654 max_machines,
1655 })
1656}
1657
1658pub fn select_family(requirements: &WorkloadRequirements) -> InstanceFamily {
1664 if requirements.gpu.is_some() {
1666 return InstanceFamily::GpuCompute;
1667 }
1668
1669 if requirements.max_ephemeral_storage_bytes > STORAGE_OPTIMIZED_THRESHOLD {
1671 return InstanceFamily::StorageOptimized;
1672 }
1673
1674 if requirements.total_cpu_at_max < 2.0 {
1676 return InstanceFamily::Burstable;
1677 }
1678
1679 InstanceFamily::GeneralPurpose
1681}
1682
1683fn compute_max_machines(requirements: &WorkloadRequirements, instance: &InstanceTypeSpec) -> u32 {
1685 let cpu_with_headroom = requirements.total_cpu_at_max * WORKLOAD_HEADROOM_FACTOR;
1686 let cpu_machines = (cpu_with_headroom / allocatable_cpu(instance)).ceil() as u32;
1687
1688 let mem_with_headroom =
1689 requirements.total_memory_bytes_at_max as f64 * WORKLOAD_HEADROOM_FACTOR;
1690 let mem_machines =
1691 (mem_with_headroom / allocatable_memory_bytes(instance) as f64).ceil() as u32;
1692
1693 cpu_machines
1695 .max(mem_machines)
1696 .max(1)
1697 .min(MAX_MACHINES_PER_CLUSTER)
1698}
1699
1700fn compute_min_machines(
1702 requirements: &WorkloadRequirements,
1703 instance: &InstanceTypeSpec,
1704 max_machines: u32,
1705) -> u32 {
1706 let cpu_with_headroom = requirements.total_cpu_at_desired * WORKLOAD_HEADROOM_FACTOR;
1707 let cpu_machines = (cpu_with_headroom / allocatable_cpu(instance)).ceil() as u32;
1708
1709 let mem_with_headroom =
1710 requirements.total_memory_bytes_at_desired as f64 * WORKLOAD_HEADROOM_FACTOR;
1711 let mem_machines =
1712 (mem_with_headroom / allocatable_memory_bytes(instance) as f64).ceil() as u32;
1713
1714 cpu_machines
1715 .max(mem_machines)
1716 .max(1)
1717 .min(2)
1718 .min(max_machines)
1719}
1720
1721fn desired_target_machines(requirements: &WorkloadRequirements) -> u32 {
1722 if requirements.total_cpu_at_desired >= 2.0
1723 || requirements.total_memory_bytes_at_desired >= 4 * GI
1724 {
1725 2
1726 } else {
1727 1
1728 }
1729}
1730
1731pub(crate) fn allocatable_cpu(instance: &InstanceTypeSpec) -> f64 {
1732 (instance.vcpu as f64 - SYSTEM_RESERVE_CPU).max(0.25)
1733}
1734
1735pub(crate) fn allocatable_memory_bytes(instance: &InstanceTypeSpec) -> u64 {
1736 instance
1737 .memory_bytes
1738 .saturating_sub(system_reserve_memory_bytes(instance.memory_bytes))
1739 .max(256 * MI)
1740}
1741
1742fn system_reserve_memory_bytes(memory_bytes: u64) -> u64 {
1743 if memory_bytes < 4 * GI {
1744 256 * MI
1745 } else if memory_bytes < 16 * GI {
1746 512 * MI
1747 } else {
1748 GI
1749 }
1750}
1751
1752#[cfg(test)]
1757mod tests {
1758 use super::*;
1759 use crate::BinaryTarget;
1760
1761 #[test]
1764 fn test_parse_cpu_plain() {
1765 assert_eq!(parse_cpu("1").unwrap(), 1.0);
1766 assert_eq!(parse_cpu("0.5").unwrap(), 0.5);
1767 assert_eq!(parse_cpu("2.0").unwrap(), 2.0);
1768 assert_eq!(parse_cpu("16").unwrap(), 16.0);
1769 }
1770
1771 #[test]
1772 fn test_parse_cpu_millicore() {
1773 assert_eq!(parse_cpu("500m").unwrap(), 0.5);
1774 assert_eq!(parse_cpu("250m").unwrap(), 0.25);
1775 assert_eq!(parse_cpu("1000m").unwrap(), 1.0);
1776 assert_eq!(parse_cpu("100m").unwrap(), 0.1);
1777 }
1778
1779 #[test]
1780 fn test_parse_cpu_invalid() {
1781 assert!(parse_cpu("").is_err());
1782 assert!(parse_cpu("abc").is_err());
1783 assert!(parse_cpu("m").is_err());
1784 }
1785
1786 #[test]
1787 fn test_parse_memory_binary_suffixes() {
1788 assert_eq!(parse_memory_bytes("1Ki").unwrap(), 1024);
1789 assert_eq!(parse_memory_bytes("1Mi").unwrap(), 1024 * 1024);
1790 assert_eq!(parse_memory_bytes("1Gi").unwrap(), 1024 * 1024 * 1024);
1791 assert_eq!(parse_memory_bytes("4Gi").unwrap(), 4 * 1024 * 1024 * 1024);
1792 assert_eq!(parse_memory_bytes("512Mi").unwrap(), 512 * 1024 * 1024);
1793 assert_eq!(
1794 parse_memory_bytes("1Ti").unwrap(),
1795 1024u64 * 1024 * 1024 * 1024
1796 );
1797 }
1798
1799 #[test]
1800 fn test_parse_memory_decimal_suffixes() {
1801 assert_eq!(parse_memory_bytes("1k").unwrap(), 1000);
1802 assert_eq!(parse_memory_bytes("1M").unwrap(), 1_000_000);
1803 assert_eq!(parse_memory_bytes("1G").unwrap(), 1_000_000_000);
1804 assert_eq!(parse_memory_bytes("1T").unwrap(), 1_000_000_000_000);
1805 }
1806
1807 #[test]
1808 fn test_parse_memory_plain_bytes() {
1809 assert_eq!(parse_memory_bytes("1024").unwrap(), 1024);
1810 assert_eq!(parse_memory_bytes("0").unwrap(), 0);
1811 }
1812
1813 #[test]
1814 fn test_parse_memory_invalid() {
1815 assert!(parse_memory_bytes("").is_err());
1816 assert!(parse_memory_bytes("abc").is_err());
1817 assert!(parse_memory_bytes("Gi").is_err());
1818 }
1819
1820 #[test]
1821 fn test_parse_memory_fractional() {
1822 assert_eq!(parse_memory_bytes("0.5Gi").unwrap(), GI / 2);
1823 assert_eq!(parse_memory_bytes("1.5Gi").unwrap(), GI + GI / 2);
1824 }
1825
1826 #[test]
1829 fn test_catalog_has_entries_for_all_cloud_platforms() {
1830 assert!(!catalog_for_platform(Platform::Aws).is_empty());
1831 assert!(!catalog_for_platform(Platform::Gcp).is_empty());
1832 assert!(!catalog_for_platform(Platform::Azure).is_empty());
1833 }
1834
1835 #[test]
1836 fn test_catalog_no_entries_for_non_cloud_platforms() {
1837 assert!(catalog_for_platform(Platform::Local).is_empty());
1838 assert!(catalog_for_platform(Platform::Kubernetes).is_empty());
1839 }
1840
1841 #[test]
1842 fn test_find_known_instance_type() {
1843 let spec =
1844 find_instance_type(Platform::Aws, "m7g.2xlarge").expect("should find m7g.2xlarge");
1845 assert_eq!(spec.vcpu, 8);
1846 assert_eq!(spec.memory_bytes, 32 * GI);
1847 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1848 }
1849
1850 #[test]
1851 fn test_find_aws_c8i_nested_virt_instance_type() {
1852 let spec = find_instance_type(Platform::Aws, "c8i.large").expect("should find c8i.large");
1853 assert_eq!(spec.vcpu, 2);
1854 assert_eq!(spec.memory_bytes, 4 * GI);
1855 assert_eq!(spec.family, InstanceFamily::ComputeOptimized);
1856 assert_eq!(spec.architecture, Architecture::X86_64);
1857 assert!(spec.is_nested_virt_capable());
1858 }
1859
1860 #[test]
1861 fn test_find_unknown_instance_type() {
1862 assert!(find_instance_type(Platform::Aws, "nonexistent.xlarge").is_none());
1863 }
1864
1865 #[test]
1866 fn test_find_wrong_platform() {
1867 assert!(find_instance_type(Platform::Gcp, "m7g.2xlarge").is_none());
1868 }
1869
1870 #[test]
1871 fn test_to_machine_profile() {
1872 let spec = find_instance_type(Platform::Aws, "m7g.2xlarge").unwrap();
1873 let profile = spec.to_machine_profile();
1874 assert_eq!(profile.cpu, "8.0");
1875 assert_eq!(profile.memory_bytes, 32 * GI);
1876 assert_eq!(profile.ephemeral_storage_bytes, 20 * GI);
1877 assert!(profile.gpu.is_none());
1878 }
1879
1880 #[test]
1881 fn test_to_machine_profile_with_gpu() {
1882 let spec = find_instance_type(Platform::Aws, "p4d.24xlarge").unwrap();
1883 let profile = spec.to_machine_profile();
1884 let gpu = profile.gpu.as_ref().expect("should have GPU");
1885 assert_eq!(gpu.gpu_type, "nvidia-a100");
1886 assert_eq!(gpu.count, 8);
1887 }
1888
1889 #[test]
1892 fn small_cpu_with_large_memory_falls_back_from_burstable() {
1893 let requirements = WorkloadRequirements {
1894 total_cpu_at_desired: 0.5,
1895 total_memory_bytes_at_desired: 8 * GI,
1896 total_cpu_at_max: 0.5,
1897 total_memory_bytes_at_max: 8 * GI,
1898 max_cpu_per_container: 0.5,
1899 max_memory_per_container: 8 * GI,
1900 max_ephemeral_storage_bytes: 0,
1901 gpu: None,
1902 architecture: Some(Architecture::X86_64),
1903 nested_virt: false,
1904 };
1905 let selected = select_instance_type(Platform::Gcp, &requirements).unwrap();
1906 let machine = find_instance_type(Platform::Gcp, selected.instance_type).unwrap();
1907 assert!(
1908 allocatable_memory_bytes(machine) as f64 >= 8.0 * GI as f64 * WORKLOAD_HEADROOM_FACTOR
1909 );
1910 }
1911
1912 #[test]
1913 fn test_select_burstable_for_small_workload() {
1914 let req = WorkloadRequirements {
1915 total_cpu_at_desired: 1.0,
1916 total_memory_bytes_at_desired: 2 * GI,
1917 total_cpu_at_max: 1.0,
1918 total_memory_bytes_at_max: 2 * GI,
1919 max_cpu_per_container: 0.5,
1920 max_memory_per_container: 1 * GI,
1921 max_ephemeral_storage_bytes: 10 * GI,
1922 gpu: None,
1923 architecture: None,
1924 nested_virt: false,
1925 };
1926 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1927 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1928 assert_eq!(spec.family, InstanceFamily::Burstable);
1929 }
1930
1931 #[test]
1932 fn test_selects_smallest_burstable_machine_with_real_headroom() {
1933 let req = WorkloadRequirements {
1934 total_cpu_at_desired: 1.0,
1935 total_memory_bytes_at_desired: 2 * GI,
1936 total_cpu_at_max: 1.0,
1937 total_memory_bytes_at_max: 2 * GI,
1938 max_cpu_per_container: 1.0,
1939 max_memory_per_container: 2 * GI,
1940 max_ephemeral_storage_bytes: 10 * GI,
1941 gpu: None,
1942 architecture: None,
1943 nested_virt: false,
1944 };
1945
1946 let selection = select_instance_type(Platform::Aws, &req).unwrap();
1947
1948 assert_eq!(selection.instance_type, "t4g.medium");
1949 assert_eq!(selection.min_machines, 1);
1950 assert_eq!(selection.max_machines, 1);
1951 }
1952
1953 #[test]
1954 fn test_select_general_purpose_for_standard_workload() {
1955 let req = WorkloadRequirements {
1957 total_cpu_at_desired: 20.0,
1958 total_memory_bytes_at_desired: 80 * GI,
1959 total_cpu_at_max: 20.0,
1960 total_memory_bytes_at_max: 80 * GI,
1961 max_cpu_per_container: 2.0,
1962 max_memory_per_container: 8 * GI,
1963 max_ephemeral_storage_bytes: 10 * GI,
1964 gpu: None,
1965 architecture: None,
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::GeneralPurpose);
1971 }
1972
1973 #[test]
1974 fn test_select_general_purpose_even_for_cpu_heavy() {
1975 let req = WorkloadRequirements {
1977 total_cpu_at_desired: 20.0,
1978 total_memory_bytes_at_desired: 20 * GI,
1979 total_cpu_at_max: 20.0,
1980 total_memory_bytes_at_max: 20 * GI,
1981 max_cpu_per_container: 2.0,
1982 max_memory_per_container: 2 * GI,
1983 max_ephemeral_storage_bytes: 10 * GI,
1984 gpu: None,
1985 architecture: None,
1986 nested_virt: false,
1987 };
1988 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1989 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1990 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1991 }
1992
1993 #[test]
1994 fn test_select_storage_optimized_for_large_ephemeral() {
1995 let req = WorkloadRequirements {
1996 total_cpu_at_desired: 8.0,
1997 total_memory_bytes_at_desired: 32 * GI,
1998 total_cpu_at_max: 8.0,
1999 total_memory_bytes_at_max: 32 * GI,
2000 max_cpu_per_container: 2.0,
2001 max_memory_per_container: 8 * GI,
2002 max_ephemeral_storage_bytes: 500 * GI,
2003 gpu: None,
2004 architecture: Some(Architecture::X86_64),
2005 nested_virt: false,
2006 };
2007 let sel = select_instance_type(Platform::Aws, &req).unwrap();
2008 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2009 assert_eq!(spec.family, InstanceFamily::StorageOptimized);
2010 }
2011
2012 #[test]
2013 fn test_select_configurable_storage_above_fixed_local_catalog() {
2014 let req = WorkloadRequirements {
2015 total_cpu_at_desired: 8.0,
2016 total_memory_bytes_at_desired: 32 * GI,
2017 total_cpu_at_max: 8.0,
2018 total_memory_bytes_at_max: 32 * GI,
2019 max_cpu_per_container: 2.0,
2020 max_memory_per_container: 8 * GI,
2021 max_ephemeral_storage_bytes: 8_000 * GI,
2022 gpu: None,
2023 architecture: Some(Architecture::X86_64),
2024 nested_virt: false,
2025 };
2026 let sel = select_instance_type(Platform::Aws, &req).unwrap();
2027 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2028 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
2029 assert_eq!(sel.profile.ephemeral_storage_bytes, 8_000 * GI);
2030 }
2031
2032 #[test]
2033 fn test_configurable_storage_fallback_retains_standard_vcpu_cap() {
2034 let req = WorkloadRequirements {
2035 total_cpu_at_desired: 70.0,
2036 total_memory_bytes_at_desired: 140 * GI,
2037 total_cpu_at_max: 70.0,
2038 total_memory_bytes_at_max: 140 * GI,
2039 max_cpu_per_container: 2.0,
2040 max_memory_per_container: 4 * GI,
2041 max_ephemeral_storage_bytes: 8_000 * GI,
2042 gpu: None,
2043 architecture: Some(Architecture::X86_64),
2044 nested_virt: false,
2045 };
2046
2047 let sel = select_instance_type(Platform::Aws, &req).unwrap();
2048 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2049 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
2050 assert!(spec.vcpu <= MAX_STANDARD_VCPU);
2051 assert!(sel.max_machines > 1);
2052 }
2053
2054 #[test]
2055 fn test_nested_virtualization_storage_falls_back_to_configurable_disk() {
2056 for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
2057 let req = WorkloadRequirements {
2058 total_cpu_at_desired: 2.0,
2059 total_memory_bytes_at_desired: 8 * GI,
2060 total_cpu_at_max: 2.0,
2061 total_memory_bytes_at_max: 8 * GI,
2062 max_cpu_per_container: 2.0,
2063 max_memory_per_container: 8 * GI,
2064 max_ephemeral_storage_bytes: 500 * GI,
2065 gpu: None,
2066 architecture: Some(Architecture::X86_64),
2067 nested_virt: true,
2068 };
2069
2070 let sel = select_instance_type(platform, &req)
2071 .unwrap_or_else(|error| panic!("{platform} should support fallback: {error}"));
2072 let spec = find_instance_type(platform, sel.instance_type).unwrap();
2073 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
2074 assert!(spec.is_nested_virt_capable());
2075 assert_eq!(sel.profile.ephemeral_storage_bytes, 500 * GI);
2076 }
2077 }
2078
2079 #[test]
2080 fn unset_architecture_resolves_to_the_one_offering_nested_virtualization() {
2081 let req = WorkloadRequirements {
2082 total_cpu_at_desired: 2.0,
2083 total_memory_bytes_at_desired: 4 * GI,
2084 total_cpu_at_max: 2.0,
2085 total_memory_bytes_at_max: 4 * GI,
2086 max_cpu_per_container: 2.0,
2087 max_memory_per_container: 4 * GI,
2088 max_ephemeral_storage_bytes: 20 * GI,
2089 gpu: None,
2090 architecture: None,
2091 nested_virt: true,
2092 };
2093
2094 for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
2095 let selection = select_instance_type(platform, &req)
2096 .unwrap_or_else(|message| panic!("{platform}: {message}"));
2097 let spec = find_instance_type(platform, selection.instance_type)
2098 .expect("selected machine should exist in the catalog");
2099 assert!(spec.is_nested_virt_capable(), "{platform}: {}", spec.name);
2100 assert_eq!(spec.architecture, Architecture::X86_64, "{platform}");
2101 assert_eq!(selection.profile.architecture, Some(Architecture::X86_64));
2102 }
2103
2104 let oversized = WorkloadRequirements {
2107 max_cpu_per_container: 1000.0,
2108 total_cpu_at_desired: 1000.0,
2109 total_cpu_at_max: 1000.0,
2110 ..req.clone()
2111 };
2112 let message = select_instance_type(Platform::Aws, &oversized)
2113 .expect_err("no AWS machine fits 1000 vCPU per replica");
2114 assert!(message.starts_with("Arm64: architecture Arm64 is unavailable"), "{message}");
2115 assert!(
2116 message.contains("; X86_64: no aws machine can fit one replica"),
2117 "{message}"
2118 );
2119
2120 let arm = WorkloadRequirements {
2122 architecture: Some(Architecture::Arm64),
2123 ..req
2124 };
2125 assert!(select_instance_type(Platform::Aws, &arm).is_err());
2126 }
2127
2128 #[test]
2129 fn test_configurable_disk_rejects_capacity_above_provider_limit() {
2130 for (platform, requested) in [
2131 (Platform::Aws, 100 * TI),
2132 (Platform::Gcp, 100 * TI),
2133 (Platform::Azure, 8_000 * GI),
2134 ] {
2135 let req = WorkloadRequirements {
2136 total_cpu_at_desired: 2.0,
2137 total_memory_bytes_at_desired: 8 * GI,
2138 total_cpu_at_max: 2.0,
2139 total_memory_bytes_at_max: 8 * GI,
2140 max_cpu_per_container: 2.0,
2141 max_memory_per_container: 8 * GI,
2142 max_ephemeral_storage_bytes: requested,
2143 gpu: None,
2144 architecture: Some(Architecture::X86_64),
2145 nested_virt: true,
2146 };
2147
2148 assert!(select_instance_type(platform, &req).is_err());
2149 }
2150 }
2151
2152 #[test]
2153 fn test_configurable_disk_limits_account_for_controller_rounding() {
2154 for (platform, disk_limit_gib) in [
2155 (Platform::Aws, 64 * 1024),
2156 (Platform::Gcp, 64 * 1024),
2157 (Platform::Azure, 4_095),
2158 ] {
2159 let max = max_configurable_ephemeral_storage_bytes(platform).unwrap();
2160 let materialized_disk_gib = |requested_bytes: u64| {
2161 let requested_gib = requested_bytes.div_ceil(GI);
2162 (requested_gib * 5).div_ceil(4) + 12
2163 };
2164
2165 assert!(materialized_disk_gib(max) <= disk_limit_gib);
2166 assert!(materialized_disk_gib(max + 1) > disk_limit_gib);
2167 }
2168 }
2169
2170 #[test]
2171 fn test_gpu_local_storage_is_not_treated_as_resizable() {
2172 let spec = find_instance_type(Platform::Aws, "g5.xlarge").unwrap();
2173 assert_eq!(spec.family, InstanceFamily::GpuCompute);
2174 assert!(!spec.has_configurable_ephemeral_storage());
2175 }
2176
2177 #[test]
2178 fn test_select_gpu_instance() {
2179 let req = WorkloadRequirements {
2180 total_cpu_at_desired: 8.0,
2181 total_memory_bytes_at_desired: 32 * GI,
2182 total_cpu_at_max: 8.0,
2183 total_memory_bytes_at_max: 32 * GI,
2184 max_cpu_per_container: 4.0,
2185 max_memory_per_container: 16 * GI,
2186 max_ephemeral_storage_bytes: 10 * GI,
2187 gpu: Some(GpuSpec {
2188 gpu_type: "nvidia-a100".to_string(),
2189 count: 1,
2190 }),
2191 architecture: Some(Architecture::X86_64),
2192 nested_virt: false,
2193 };
2194 let sel = select_instance_type(Platform::Aws, &req).unwrap();
2195 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2196 assert_eq!(spec.family, InstanceFamily::GpuCompute);
2197 assert!(spec.gpu.is_some());
2198 }
2199
2200 #[test]
2201 fn test_select_uses_each_cloud_image_target_architecture() {
2202 let req = WorkloadRequirements {
2203 total_cpu_at_desired: 4.0,
2204 total_memory_bytes_at_desired: 16 * GI,
2205 total_cpu_at_max: 4.0,
2206 total_memory_bytes_at_max: 16 * GI,
2207 max_cpu_per_container: 1.0,
2208 max_memory_per_container: 4 * GI,
2209 max_ephemeral_storage_bytes: 10 * GI,
2210 gpu: None,
2211 architecture: None,
2212 nested_virt: false,
2213 };
2214 for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
2215 let sel = select_instance_type(platform, &req)
2216 .unwrap_or_else(|error| panic!("selection failed for {platform}: {error}"));
2217 let spec = find_instance_type(platform, sel.instance_type)
2218 .expect("selected machine should exist in the catalog");
2219 assert_eq!(
2220 Some(spec.architecture),
2221 default_architecture(platform),
2222 "machine architecture must match the image target for {platform}"
2223 );
2224 }
2225 }
2226
2227 #[test]
2228 fn test_machine_count_reasonable() {
2229 let req = WorkloadRequirements {
2231 total_cpu_at_desired: 20.0,
2232 total_memory_bytes_at_desired: 40 * GI,
2233 total_cpu_at_max: 20.0,
2234 total_memory_bytes_at_max: 40 * GI,
2235 max_cpu_per_container: 1.0,
2236 max_memory_per_container: 2 * GI,
2237 max_ephemeral_storage_bytes: 10 * GI,
2238 gpu: None,
2239 architecture: None,
2240 nested_virt: false,
2241 };
2242 let sel = select_instance_type(Platform::Aws, &req).unwrap();
2243 assert!(sel.min_machines >= 1);
2244 assert!(sel.max_machines <= MAX_MACHINES_PER_CLUSTER);
2245 assert!(sel.max_machines >= sel.min_machines);
2246 }
2247
2248 #[test]
2249 fn test_instance_size_capped_at_8_vcpu() {
2250 let req = WorkloadRequirements {
2252 total_cpu_at_desired: 70.0,
2253 total_memory_bytes_at_desired: 140 * GI,
2254 total_cpu_at_max: 70.0,
2255 total_memory_bytes_at_max: 140 * GI,
2256 max_cpu_per_container: 2.0,
2257 max_memory_per_container: 4 * GI,
2258 max_ephemeral_storage_bytes: 10 * GI,
2259 gpu: None,
2260 architecture: None,
2261 nested_virt: false,
2262 };
2263 let sel = select_instance_type(Platform::Gcp, &req).unwrap();
2264 let spec = find_instance_type(Platform::Gcp, sel.instance_type).unwrap();
2265 assert!(
2266 spec.vcpu <= MAX_STANDARD_VCPU,
2267 "selected {} with {} vCPUs, expected <= {}",
2268 spec.name,
2269 spec.vcpu,
2270 MAX_STANDARD_VCPU
2271 );
2272 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
2273 assert!(sel.max_machines > 1);
2275 }
2276
2277 #[test]
2278 fn test_larger_autoscaled_workload_gets_reasonable_instance() {
2279 let req = WorkloadRequirements {
2282 total_cpu_at_desired: 70.0,
2283 total_memory_bytes_at_desired: 140 * GI,
2284 total_cpu_at_max: 70.0, total_memory_bytes_at_max: 140 * GI, max_cpu_per_container: 2.0,
2287 max_memory_per_container: 4 * GI,
2288 max_ephemeral_storage_bytes: 20 * GI,
2289 gpu: None,
2290 architecture: None,
2291 nested_virt: false,
2292 };
2293 let sel = select_instance_type(Platform::Gcp, &req).unwrap();
2294 assert_eq!(sel.instance_type, "n2-standard-8");
2296 assert!(sel.max_machines >= 2);
2297 }
2298
2299 #[test]
2306 fn test_select_aws_picks_m8i_when_nested_virt_required() {
2307 let req = WorkloadRequirements {
2308 total_cpu_at_desired: 4.0,
2309 total_memory_bytes_at_desired: 8 * GI,
2310 total_cpu_at_max: 4.0,
2311 total_memory_bytes_at_max: 8 * GI,
2312 max_cpu_per_container: 4.0,
2313 max_memory_per_container: 8 * GI,
2314 max_ephemeral_storage_bytes: 10 * GI,
2315 gpu: None,
2316 architecture: Some(Architecture::X86_64),
2317 nested_virt: true,
2318 };
2319 let sel = select_instance_type(Platform::Aws, &req).unwrap();
2320 assert!(
2321 sel.instance_type.starts_with("m8i.")
2322 || sel.instance_type.starts_with("c8i.")
2323 || sel.instance_type.starts_with("r8i."),
2324 "expected an m8i/c8i/r8i instance, got {}",
2325 sel.instance_type
2326 );
2327 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2328 assert!(spec.is_nested_virt_capable());
2329 }
2330
2331 #[test]
2332 fn test_select_gcp_picks_n2_when_nested_virt_required() {
2333 let req = WorkloadRequirements {
2334 total_cpu_at_desired: 4.0,
2335 total_memory_bytes_at_desired: 8 * GI,
2336 total_cpu_at_max: 4.0,
2337 total_memory_bytes_at_max: 8 * GI,
2338 max_cpu_per_container: 4.0,
2339 max_memory_per_container: 8 * GI,
2340 max_ephemeral_storage_bytes: 0,
2341 architecture: Some(Architecture::X86_64),
2342 gpu: None,
2343 nested_virt: true,
2344 };
2345
2346 let selection = select_instance_type(Platform::Gcp, &req).unwrap();
2347 assert_eq!(selection.instance_type, "n2-standard-8");
2348 assert!(find_instance_type(Platform::Gcp, selection.instance_type)
2349 .unwrap()
2350 .is_nested_virt_capable());
2351 }
2352
2353 #[test]
2354 fn test_select_azure_picks_dsv5_when_nested_virt_required() {
2355 let req = WorkloadRequirements {
2356 total_cpu_at_desired: 4.0,
2357 total_memory_bytes_at_desired: 8 * GI,
2358 total_cpu_at_max: 4.0,
2359 total_memory_bytes_at_max: 8 * GI,
2360 max_cpu_per_container: 4.0,
2361 max_memory_per_container: 8 * GI,
2362 max_ephemeral_storage_bytes: 0,
2363 architecture: Some(Architecture::X86_64),
2364 gpu: None,
2365 nested_virt: true,
2366 };
2367
2368 let selection = select_instance_type(Platform::Azure, &req).unwrap();
2369 assert_eq!(selection.instance_type, "Standard_D8s_v5");
2370 assert!(find_instance_type(Platform::Azure, selection.instance_type)
2371 .unwrap()
2372 .is_nested_virt_capable());
2373 }
2374
2375 #[test]
2376 fn test_select_aws_defaults_to_image_target_architecture() {
2377 let req = WorkloadRequirements {
2378 total_cpu_at_desired: 4.0,
2379 total_memory_bytes_at_desired: 8 * GI,
2380 total_cpu_at_max: 4.0,
2381 total_memory_bytes_at_max: 8 * GI,
2382 max_cpu_per_container: 4.0,
2383 max_memory_per_container: 8 * GI,
2384 max_ephemeral_storage_bytes: 10 * GI,
2385 gpu: None,
2386 architecture: None,
2387 nested_virt: false,
2388 };
2389 let sel = select_instance_type(Platform::Aws, &req).unwrap();
2390 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2391 assert_eq!(spec.architecture, Architecture::Arm64);
2392 }
2393
2394 #[test]
2395 fn test_cloud_defaults_match_image_target_architectures() {
2396 for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
2397 let target = BinaryTarget::defaults_for_platform(platform)
2398 .into_iter()
2399 .next()
2400 .expect("managed cloud should have a default image target");
2401 let image_architecture = match target.oci_arch() {
2402 "arm64" => Architecture::Arm64,
2403 "amd64" => Architecture::X86_64,
2404 architecture => {
2405 panic!("unsupported managed-cloud image architecture {architecture}")
2406 }
2407 };
2408
2409 assert_eq!(default_architecture(platform), Some(image_architecture));
2410 }
2411 }
2412
2413 #[test]
2415 fn test_select_aws_uses_graviton_for_explicit_arm64() {
2416 let req = WorkloadRequirements {
2417 total_cpu_at_desired: 4.0,
2418 total_memory_bytes_at_desired: 8 * GI,
2419 total_cpu_at_max: 4.0,
2420 total_memory_bytes_at_max: 8 * GI,
2421 max_cpu_per_container: 4.0,
2422 max_memory_per_container: 8 * GI,
2423 max_ephemeral_storage_bytes: 10 * GI,
2424 gpu: None,
2425 architecture: Some(Architecture::Arm64),
2426 nested_virt: false,
2427 };
2428 let sel = select_instance_type(Platform::Aws, &req).unwrap();
2429 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2430 assert_eq!(spec.architecture, Architecture::Arm64);
2431 }
2432
2433 #[test]
2434 fn test_select_rejects_explicit_architecture_missing_from_cloud_catalog() {
2435 let req = WorkloadRequirements {
2436 total_cpu_at_desired: 1.0,
2437 total_memory_bytes_at_desired: 2 * GI,
2438 total_cpu_at_max: 1.0,
2439 total_memory_bytes_at_max: 2 * GI,
2440 max_cpu_per_container: 1.0,
2441 max_memory_per_container: 2 * GI,
2442 max_ephemeral_storage_bytes: 10 * GI,
2443 gpu: None,
2444 architecture: Some(Architecture::Arm64),
2445 nested_virt: false,
2446 };
2447
2448 let error = select_instance_type(Platform::Gcp, &req)
2449 .expect_err("GCP catalog has no ARM64 machine");
2450
2451 assert!(error.contains("architecture Arm64 is unavailable"));
2452 }
2453
2454 #[test]
2455 fn test_profile_has_required_fields() {
2456 let req = WorkloadRequirements {
2457 total_cpu_at_desired: 4.0,
2458 total_memory_bytes_at_desired: 16 * GI,
2459 total_cpu_at_max: 4.0,
2460 total_memory_bytes_at_max: 16 * GI,
2461 max_cpu_per_container: 1.0,
2462 max_memory_per_container: 4 * GI,
2463 max_ephemeral_storage_bytes: 10 * GI,
2464 gpu: None,
2465 architecture: None,
2466 nested_virt: false,
2467 };
2468 let sel = select_instance_type(Platform::Aws, &req).unwrap();
2469 assert!(!sel.profile.cpu.is_empty());
2470 assert!(sel.profile.memory_bytes > 0);
2471 assert!(sel.profile.ephemeral_storage_bytes > 0);
2472 }
2473
2474 #[test]
2475 fn test_error_for_unsupported_gpu_type() {
2476 let req = WorkloadRequirements {
2477 total_cpu_at_desired: 8.0,
2478 total_memory_bytes_at_desired: 32 * GI,
2479 total_cpu_at_max: 8.0,
2480 total_memory_bytes_at_max: 32 * GI,
2481 max_cpu_per_container: 4.0,
2482 max_memory_per_container: 16 * GI,
2483 max_ephemeral_storage_bytes: 10 * GI,
2484 gpu: Some(GpuSpec {
2485 gpu_type: "amd-mi300".to_string(),
2486 count: 1,
2487 }),
2488 architecture: None,
2489 nested_virt: false,
2490 };
2491 let result = select_instance_type(Platform::Aws, &req);
2492 assert!(result.is_err());
2493 }
2494
2495 #[test]
2496 fn test_catalog_instance_types_sorted_by_vcpu_within_family() {
2497 for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
2500 let entries = catalog_for_platform(platform);
2501 let mut by_family: std::collections::HashMap<_, Vec<_>> =
2502 std::collections::HashMap::new();
2503 for entry in entries {
2504 by_family
2505 .entry(format!("{:?}", entry.family))
2506 .or_default()
2507 .push(entry);
2508 }
2509 for (family, instances) in &by_family {
2510 for window in instances.windows(2) {
2511 assert!(
2512 window[0].vcpu <= window[1].vcpu,
2513 "catalog not sorted by vcpu for {platform}/{family}: {} ({}) > {} ({})",
2514 window[0].name,
2515 window[0].vcpu,
2516 window[1].name,
2517 window[1].vcpu
2518 );
2519 }
2520 }
2521 }
2522 }
2523}