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