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 is_nested_virt_capable(&self) -> bool {
174 if self.platform != Platform::Aws {
175 return false;
178 }
179 let name = self.name;
180 name.starts_with("m8i.")
181 || name.starts_with("c8i.")
182 || name.starts_with("r8i.")
183 || name.starts_with("m8i-flex.")
184 || name.starts_with("c8i-flex.")
185 || name.starts_with("r8i-flex.")
186 }
187
188 pub fn to_machine_profile(&self) -> MachineProfile {
190 MachineProfile {
191 cpu: format!("{}.0", self.vcpu),
192 memory_bytes: self.memory_bytes,
193 ephemeral_storage_bytes: self.ephemeral_storage_bytes,
194 architecture: Some(self.architecture),
195 gpu: self.gpu.map(|g| GpuSpec {
196 gpu_type: g.gpu_type.to_string(),
197 count: g.count,
198 }),
199 }
200 }
201}
202
203const KI: u64 = 1024;
205const MI: u64 = KI * 1024;
206const GI: u64 = MI * 1024;
207
208static CATALOG: &[InstanceTypeSpec] = &[
216 InstanceTypeSpec {
222 name: "t4g.micro",
223 platform: Platform::Aws,
224 family: InstanceFamily::Burstable,
225 architecture: Architecture::Arm64,
226 vcpu: 2,
227 memory_bytes: 1 * GI,
228 ephemeral_storage_bytes: 20 * GI,
229 gpu: None,
230 },
231 InstanceTypeSpec {
232 name: "t4g.small",
233 platform: Platform::Aws,
234 family: InstanceFamily::Burstable,
235 architecture: Architecture::Arm64,
236 vcpu: 2,
237 memory_bytes: 2 * GI,
238 ephemeral_storage_bytes: 20 * GI,
239 gpu: None,
240 },
241 InstanceTypeSpec {
242 name: "t4g.medium",
243 platform: Platform::Aws,
244 family: InstanceFamily::Burstable,
245 architecture: Architecture::Arm64,
246 vcpu: 2,
247 memory_bytes: 4 * GI,
248 ephemeral_storage_bytes: 20 * GI,
249 gpu: None,
250 },
251 InstanceTypeSpec {
252 name: "t4g.large",
253 platform: Platform::Aws,
254 family: InstanceFamily::Burstable,
255 architecture: Architecture::Arm64,
256 vcpu: 2,
257 memory_bytes: 8 * GI,
258 ephemeral_storage_bytes: 20 * GI,
259 gpu: None,
260 },
261 InstanceTypeSpec {
262 name: "t3.xlarge",
263 platform: Platform::Aws,
264 family: InstanceFamily::Burstable,
265 architecture: Architecture::X86_64,
266 vcpu: 4,
267 memory_bytes: 16 * GI,
268 ephemeral_storage_bytes: 20 * GI,
269 gpu: None,
270 },
271 InstanceTypeSpec {
272 name: "t4g.xlarge",
273 platform: Platform::Aws,
274 family: InstanceFamily::Burstable,
275 architecture: Architecture::Arm64,
276 vcpu: 4,
277 memory_bytes: 16 * GI,
278 ephemeral_storage_bytes: 20 * GI,
279 gpu: None,
280 },
281 InstanceTypeSpec {
283 name: "m7g.medium",
284 platform: Platform::Aws,
285 family: InstanceFamily::GeneralPurpose,
286 architecture: Architecture::Arm64,
287 vcpu: 1,
288 memory_bytes: 4 * GI,
289 ephemeral_storage_bytes: 20 * GI,
290 gpu: None,
291 },
292 InstanceTypeSpec {
293 name: "m7i.large",
294 platform: Platform::Aws,
295 family: InstanceFamily::GeneralPurpose,
296 architecture: Architecture::X86_64,
297 vcpu: 2,
298 memory_bytes: 8 * GI,
299 ephemeral_storage_bytes: 20 * GI,
300 gpu: None,
301 },
302 InstanceTypeSpec {
303 name: "m7g.large",
304 platform: Platform::Aws,
305 family: InstanceFamily::GeneralPurpose,
306 architecture: Architecture::Arm64,
307 vcpu: 2,
308 memory_bytes: 8 * GI,
309 ephemeral_storage_bytes: 20 * GI,
310 gpu: None,
311 },
312 InstanceTypeSpec {
320 name: "m8i.large",
321 platform: Platform::Aws,
322 family: InstanceFamily::GeneralPurpose,
323 architecture: Architecture::X86_64,
324 vcpu: 2,
325 memory_bytes: 8 * GI,
326 ephemeral_storage_bytes: 20 * GI,
327 gpu: None,
328 },
329 InstanceTypeSpec {
330 name: "m7i.xlarge",
331 platform: Platform::Aws,
332 family: InstanceFamily::GeneralPurpose,
333 architecture: Architecture::X86_64,
334 vcpu: 4,
335 memory_bytes: 16 * GI,
336 ephemeral_storage_bytes: 20 * GI,
337 gpu: None,
338 },
339 InstanceTypeSpec {
340 name: "m7g.xlarge",
341 platform: Platform::Aws,
342 family: InstanceFamily::GeneralPurpose,
343 architecture: Architecture::Arm64,
344 vcpu: 4,
345 memory_bytes: 16 * GI,
346 ephemeral_storage_bytes: 20 * GI,
347 gpu: None,
348 },
349 InstanceTypeSpec {
350 name: "m8i.xlarge",
351 platform: Platform::Aws,
352 family: InstanceFamily::GeneralPurpose,
353 architecture: Architecture::X86_64,
354 vcpu: 4,
355 memory_bytes: 16 * GI,
356 ephemeral_storage_bytes: 20 * GI,
357 gpu: None,
358 },
359 InstanceTypeSpec {
360 name: "m7i.2xlarge",
361 platform: Platform::Aws,
362 family: InstanceFamily::GeneralPurpose,
363 architecture: Architecture::X86_64,
364 vcpu: 8,
365 memory_bytes: 32 * GI,
366 ephemeral_storage_bytes: 20 * GI,
367 gpu: None,
368 },
369 InstanceTypeSpec {
370 name: "m7g.2xlarge",
371 platform: Platform::Aws,
372 family: InstanceFamily::GeneralPurpose,
373 architecture: Architecture::Arm64,
374 vcpu: 8,
375 memory_bytes: 32 * GI,
376 ephemeral_storage_bytes: 20 * GI,
377 gpu: None,
378 },
379 InstanceTypeSpec {
380 name: "m8i.2xlarge",
381 platform: Platform::Aws,
382 family: InstanceFamily::GeneralPurpose,
383 architecture: Architecture::X86_64,
384 vcpu: 8,
385 memory_bytes: 32 * GI,
386 ephemeral_storage_bytes: 20 * GI,
387 gpu: None,
388 },
389 InstanceTypeSpec {
390 name: "m7i.4xlarge",
391 platform: Platform::Aws,
392 family: InstanceFamily::GeneralPurpose,
393 architecture: Architecture::X86_64,
394 vcpu: 16,
395 memory_bytes: 64 * GI,
396 ephemeral_storage_bytes: 20 * GI,
397 gpu: None,
398 },
399 InstanceTypeSpec {
400 name: "m7g.4xlarge",
401 platform: Platform::Aws,
402 family: InstanceFamily::GeneralPurpose,
403 architecture: Architecture::Arm64,
404 vcpu: 16,
405 memory_bytes: 64 * GI,
406 ephemeral_storage_bytes: 20 * GI,
407 gpu: None,
408 },
409 InstanceTypeSpec {
410 name: "m8i.4xlarge",
411 platform: Platform::Aws,
412 family: InstanceFamily::GeneralPurpose,
413 architecture: Architecture::X86_64,
414 vcpu: 16,
415 memory_bytes: 64 * GI,
416 ephemeral_storage_bytes: 20 * GI,
417 gpu: None,
418 },
419 InstanceTypeSpec {
421 name: "c7g.medium",
422 platform: Platform::Aws,
423 family: InstanceFamily::ComputeOptimized,
424 architecture: Architecture::Arm64,
425 vcpu: 1,
426 memory_bytes: 2 * GI,
427 ephemeral_storage_bytes: 20 * GI,
428 gpu: None,
429 },
430 InstanceTypeSpec {
431 name: "c7g.large",
432 platform: Platform::Aws,
433 family: InstanceFamily::ComputeOptimized,
434 architecture: Architecture::Arm64,
435 vcpu: 2,
436 memory_bytes: 4 * GI,
437 ephemeral_storage_bytes: 20 * GI,
438 gpu: None,
439 },
440 InstanceTypeSpec {
441 name: "c8i.large",
442 platform: Platform::Aws,
443 family: InstanceFamily::ComputeOptimized,
444 architecture: Architecture::X86_64,
445 vcpu: 2,
446 memory_bytes: 4 * GI,
447 ephemeral_storage_bytes: 20 * GI,
448 gpu: None,
449 },
450 InstanceTypeSpec {
451 name: "c7g.xlarge",
452 platform: Platform::Aws,
453 family: InstanceFamily::ComputeOptimized,
454 architecture: Architecture::Arm64,
455 vcpu: 4,
456 memory_bytes: 8 * GI,
457 ephemeral_storage_bytes: 20 * GI,
458 gpu: None,
459 },
460 InstanceTypeSpec {
461 name: "c8i.xlarge",
462 platform: Platform::Aws,
463 family: InstanceFamily::ComputeOptimized,
464 architecture: Architecture::X86_64,
465 vcpu: 4,
466 memory_bytes: 8 * GI,
467 ephemeral_storage_bytes: 20 * GI,
468 gpu: None,
469 },
470 InstanceTypeSpec {
471 name: "c7g.2xlarge",
472 platform: Platform::Aws,
473 family: InstanceFamily::ComputeOptimized,
474 architecture: Architecture::Arm64,
475 vcpu: 8,
476 memory_bytes: 16 * GI,
477 ephemeral_storage_bytes: 20 * GI,
478 gpu: None,
479 },
480 InstanceTypeSpec {
481 name: "c8i.2xlarge",
482 platform: Platform::Aws,
483 family: InstanceFamily::ComputeOptimized,
484 architecture: Architecture::X86_64,
485 vcpu: 8,
486 memory_bytes: 16 * GI,
487 ephemeral_storage_bytes: 20 * GI,
488 gpu: None,
489 },
490 InstanceTypeSpec {
491 name: "c7g.4xlarge",
492 platform: Platform::Aws,
493 family: InstanceFamily::ComputeOptimized,
494 architecture: Architecture::Arm64,
495 vcpu: 16,
496 memory_bytes: 32 * GI,
497 ephemeral_storage_bytes: 20 * GI,
498 gpu: None,
499 },
500 InstanceTypeSpec {
501 name: "c8i.4xlarge",
502 platform: Platform::Aws,
503 family: InstanceFamily::ComputeOptimized,
504 architecture: Architecture::X86_64,
505 vcpu: 16,
506 memory_bytes: 32 * GI,
507 ephemeral_storage_bytes: 20 * GI,
508 gpu: None,
509 },
510 InstanceTypeSpec {
512 name: "r7g.medium",
513 platform: Platform::Aws,
514 family: InstanceFamily::MemoryOptimized,
515 architecture: Architecture::Arm64,
516 vcpu: 1,
517 memory_bytes: 8 * GI,
518 ephemeral_storage_bytes: 20 * GI,
519 gpu: None,
520 },
521 InstanceTypeSpec {
522 name: "r7g.large",
523 platform: Platform::Aws,
524 family: InstanceFamily::MemoryOptimized,
525 architecture: Architecture::Arm64,
526 vcpu: 2,
527 memory_bytes: 16 * GI,
528 ephemeral_storage_bytes: 20 * GI,
529 gpu: None,
530 },
531 InstanceTypeSpec {
532 name: "r7g.xlarge",
533 platform: Platform::Aws,
534 family: InstanceFamily::MemoryOptimized,
535 architecture: Architecture::Arm64,
536 vcpu: 4,
537 memory_bytes: 32 * GI,
538 ephemeral_storage_bytes: 20 * GI,
539 gpu: None,
540 },
541 InstanceTypeSpec {
542 name: "r7g.2xlarge",
543 platform: Platform::Aws,
544 family: InstanceFamily::MemoryOptimized,
545 architecture: Architecture::Arm64,
546 vcpu: 8,
547 memory_bytes: 64 * GI,
548 ephemeral_storage_bytes: 20 * GI,
549 gpu: None,
550 },
551 InstanceTypeSpec {
552 name: "r7g.4xlarge",
553 platform: Platform::Aws,
554 family: InstanceFamily::MemoryOptimized,
555 architecture: Architecture::Arm64,
556 vcpu: 16,
557 memory_bytes: 128 * GI,
558 ephemeral_storage_bytes: 20 * GI,
559 gpu: None,
560 },
561 InstanceTypeSpec {
563 name: "i4i.xlarge",
564 platform: Platform::Aws,
565 family: InstanceFamily::StorageOptimized,
566 architecture: Architecture::X86_64,
567 vcpu: 4,
568 memory_bytes: 32 * GI,
569 ephemeral_storage_bytes: 937 * GI,
570 gpu: None,
571 },
572 InstanceTypeSpec {
573 name: "i4i.2xlarge",
574 platform: Platform::Aws,
575 family: InstanceFamily::StorageOptimized,
576 architecture: Architecture::X86_64,
577 vcpu: 8,
578 memory_bytes: 64 * GI,
579 ephemeral_storage_bytes: 1875 * GI,
580 gpu: None,
581 },
582 InstanceTypeSpec {
583 name: "i4i.4xlarge",
584 platform: Platform::Aws,
585 family: InstanceFamily::StorageOptimized,
586 architecture: Architecture::X86_64,
587 vcpu: 16,
588 memory_bytes: 128 * GI,
589 ephemeral_storage_bytes: 3750 * GI,
590 gpu: None,
591 },
592 InstanceTypeSpec {
593 name: "i4i.8xlarge",
594 platform: Platform::Aws,
595 family: InstanceFamily::StorageOptimized,
596 architecture: Architecture::X86_64,
597 vcpu: 32,
598 memory_bytes: 256 * GI,
599 ephemeral_storage_bytes: 7500 * GI,
600 gpu: None,
601 },
602 InstanceTypeSpec {
604 name: "g5.xlarge",
605 platform: Platform::Aws,
606 family: InstanceFamily::GpuCompute,
607 architecture: Architecture::X86_64,
608 vcpu: 4,
609 memory_bytes: 16 * GI,
610 ephemeral_storage_bytes: 250 * GI,
611 gpu: Some(CatalogGpu {
612 gpu_type: "nvidia-t4",
613 count: 1,
614 }),
615 },
616 InstanceTypeSpec {
617 name: "g5.2xlarge",
618 platform: Platform::Aws,
619 family: InstanceFamily::GpuCompute,
620 architecture: Architecture::X86_64,
621 vcpu: 8,
622 memory_bytes: 32 * GI,
623 ephemeral_storage_bytes: 450 * GI,
624 gpu: Some(CatalogGpu {
625 gpu_type: "nvidia-t4",
626 count: 1,
627 }),
628 },
629 InstanceTypeSpec {
631 name: "p4d.24xlarge",
632 platform: Platform::Aws,
633 family: InstanceFamily::GpuCompute,
634 architecture: Architecture::X86_64,
635 vcpu: 96,
636 memory_bytes: 1152 * GI,
637 ephemeral_storage_bytes: 8000 * GI,
638 gpu: Some(CatalogGpu {
639 gpu_type: "nvidia-a100",
640 count: 8,
641 }),
642 },
643 InstanceTypeSpec {
645 name: "p5.48xlarge",
646 platform: Platform::Aws,
647 family: InstanceFamily::GpuCompute,
648 architecture: Architecture::X86_64,
649 vcpu: 192,
650 memory_bytes: 2048 * GI,
651 ephemeral_storage_bytes: 8000 * GI,
652 gpu: Some(CatalogGpu {
653 gpu_type: "nvidia-h100",
654 count: 8,
655 }),
656 },
657 InstanceTypeSpec {
663 name: "e2-micro",
664 platform: Platform::Gcp,
665 family: InstanceFamily::Burstable,
666 architecture: Architecture::X86_64,
667 vcpu: 2,
668 memory_bytes: 1 * GI,
669 ephemeral_storage_bytes: 20 * GI,
670 gpu: None,
671 },
672 InstanceTypeSpec {
673 name: "e2-small",
674 platform: Platform::Gcp,
675 family: InstanceFamily::Burstable,
676 architecture: Architecture::X86_64,
677 vcpu: 2,
678 memory_bytes: 2 * GI,
679 ephemeral_storage_bytes: 20 * GI,
680 gpu: None,
681 },
682 InstanceTypeSpec {
683 name: "e2-medium",
684 platform: Platform::Gcp,
685 family: InstanceFamily::Burstable,
686 architecture: Architecture::X86_64,
687 vcpu: 2,
688 memory_bytes: 4 * GI,
689 ephemeral_storage_bytes: 20 * GI,
690 gpu: None,
691 },
692 InstanceTypeSpec {
694 name: "n2-standard-2",
695 platform: Platform::Gcp,
696 family: InstanceFamily::GeneralPurpose,
697 architecture: Architecture::X86_64,
698 vcpu: 2,
699 memory_bytes: 8 * GI,
700 ephemeral_storage_bytes: 20 * GI,
701 gpu: None,
702 },
703 InstanceTypeSpec {
704 name: "n2-standard-4",
705 platform: Platform::Gcp,
706 family: InstanceFamily::GeneralPurpose,
707 architecture: Architecture::X86_64,
708 vcpu: 4,
709 memory_bytes: 16 * GI,
710 ephemeral_storage_bytes: 20 * GI,
711 gpu: None,
712 },
713 InstanceTypeSpec {
714 name: "n2-standard-8",
715 platform: Platform::Gcp,
716 family: InstanceFamily::GeneralPurpose,
717 architecture: Architecture::X86_64,
718 vcpu: 8,
719 memory_bytes: 32 * GI,
720 ephemeral_storage_bytes: 20 * GI,
721 gpu: None,
722 },
723 InstanceTypeSpec {
724 name: "n2-standard-16",
725 platform: Platform::Gcp,
726 family: InstanceFamily::GeneralPurpose,
727 architecture: Architecture::X86_64,
728 vcpu: 16,
729 memory_bytes: 64 * GI,
730 ephemeral_storage_bytes: 20 * GI,
731 gpu: None,
732 },
733 InstanceTypeSpec {
735 name: "c3-standard-4",
736 platform: Platform::Gcp,
737 family: InstanceFamily::ComputeOptimized,
738 architecture: Architecture::X86_64,
739 vcpu: 4,
740 memory_bytes: 8 * GI,
741 ephemeral_storage_bytes: 20 * GI,
742 gpu: None,
743 },
744 InstanceTypeSpec {
745 name: "c3-standard-8",
746 platform: Platform::Gcp,
747 family: InstanceFamily::ComputeOptimized,
748 architecture: Architecture::X86_64,
749 vcpu: 8,
750 memory_bytes: 16 * GI,
751 ephemeral_storage_bytes: 20 * GI,
752 gpu: None,
753 },
754 InstanceTypeSpec {
756 name: "n2-highmem-2",
757 platform: Platform::Gcp,
758 family: InstanceFamily::MemoryOptimized,
759 architecture: Architecture::X86_64,
760 vcpu: 2,
761 memory_bytes: 16 * GI,
762 ephemeral_storage_bytes: 20 * GI,
763 gpu: None,
764 },
765 InstanceTypeSpec {
766 name: "n2-highmem-4",
767 platform: Platform::Gcp,
768 family: InstanceFamily::MemoryOptimized,
769 architecture: Architecture::X86_64,
770 vcpu: 4,
771 memory_bytes: 32 * GI,
772 ephemeral_storage_bytes: 20 * GI,
773 gpu: None,
774 },
775 InstanceTypeSpec {
776 name: "n2-highmem-8",
777 platform: Platform::Gcp,
778 family: InstanceFamily::MemoryOptimized,
779 architecture: Architecture::X86_64,
780 vcpu: 8,
781 memory_bytes: 64 * GI,
782 ephemeral_storage_bytes: 20 * GI,
783 gpu: None,
784 },
785 InstanceTypeSpec {
786 name: "n2-highmem-16",
787 platform: Platform::Gcp,
788 family: InstanceFamily::MemoryOptimized,
789 architecture: Architecture::X86_64,
790 vcpu: 16,
791 memory_bytes: 128 * GI,
792 ephemeral_storage_bytes: 20 * GI,
793 gpu: None,
794 },
795 InstanceTypeSpec {
796 name: "n2-highmem-32",
797 platform: Platform::Gcp,
798 family: InstanceFamily::MemoryOptimized,
799 architecture: Architecture::X86_64,
800 vcpu: 32,
801 memory_bytes: 256 * GI,
802 ephemeral_storage_bytes: 20 * GI,
803 gpu: None,
804 },
805 InstanceTypeSpec {
807 name: "c3d-standard-8",
808 platform: Platform::Gcp,
809 family: InstanceFamily::StorageOptimized,
810 architecture: Architecture::X86_64,
811 vcpu: 8,
812 memory_bytes: 32 * GI,
813 ephemeral_storage_bytes: 480 * GI,
814 gpu: None,
815 },
816 InstanceTypeSpec {
817 name: "c3d-standard-16",
818 platform: Platform::Gcp,
819 family: InstanceFamily::StorageOptimized,
820 architecture: Architecture::X86_64,
821 vcpu: 16,
822 memory_bytes: 64 * GI,
823 ephemeral_storage_bytes: 960 * GI,
824 gpu: None,
825 },
826 InstanceTypeSpec {
827 name: "c3d-standard-30",
828 platform: Platform::Gcp,
829 family: InstanceFamily::StorageOptimized,
830 architecture: Architecture::X86_64,
831 vcpu: 30,
832 memory_bytes: 120 * GI,
833 ephemeral_storage_bytes: 1920 * GI,
834 gpu: None,
835 },
836 InstanceTypeSpec {
838 name: "n1-standard-4-t4",
839 platform: Platform::Gcp,
840 family: InstanceFamily::GpuCompute,
841 architecture: Architecture::X86_64,
842 vcpu: 4,
843 memory_bytes: 15 * GI,
844 ephemeral_storage_bytes: 100 * GI,
845 gpu: Some(CatalogGpu {
846 gpu_type: "nvidia-t4",
847 count: 1,
848 }),
849 },
850 InstanceTypeSpec {
852 name: "a2-highgpu-1g",
853 platform: Platform::Gcp,
854 family: InstanceFamily::GpuCompute,
855 architecture: Architecture::X86_64,
856 vcpu: 12,
857 memory_bytes: 85 * GI,
858 ephemeral_storage_bytes: 100 * GI,
859 gpu: Some(CatalogGpu {
860 gpu_type: "nvidia-a100",
861 count: 1,
862 }),
863 },
864 InstanceTypeSpec {
865 name: "a2-highgpu-8g",
866 platform: Platform::Gcp,
867 family: InstanceFamily::GpuCompute,
868 architecture: Architecture::X86_64,
869 vcpu: 96,
870 memory_bytes: 1360 * GI,
871 ephemeral_storage_bytes: 100 * GI,
872 gpu: Some(CatalogGpu {
873 gpu_type: "nvidia-a100",
874 count: 8,
875 }),
876 },
877 InstanceTypeSpec {
879 name: "a3-highgpu-8g",
880 platform: Platform::Gcp,
881 family: InstanceFamily::GpuCompute,
882 architecture: Architecture::X86_64,
883 vcpu: 208,
884 memory_bytes: 1872 * GI,
885 ephemeral_storage_bytes: 100 * GI,
886 gpu: Some(CatalogGpu {
887 gpu_type: "nvidia-h100",
888 count: 8,
889 }),
890 },
891 InstanceTypeSpec {
897 name: "Standard_B1s",
898 platform: Platform::Azure,
899 family: InstanceFamily::Burstable,
900 architecture: Architecture::X86_64,
901 vcpu: 1,
902 memory_bytes: 1 * GI,
903 ephemeral_storage_bytes: 20 * GI,
904 gpu: None,
905 },
906 InstanceTypeSpec {
907 name: "Standard_B2s",
908 platform: Platform::Azure,
909 family: InstanceFamily::Burstable,
910 architecture: Architecture::X86_64,
911 vcpu: 2,
912 memory_bytes: 4 * GI,
913 ephemeral_storage_bytes: 20 * GI,
914 gpu: None,
915 },
916 InstanceTypeSpec {
917 name: "Standard_B2ms",
918 platform: Platform::Azure,
919 family: InstanceFamily::Burstable,
920 architecture: Architecture::X86_64,
921 vcpu: 2,
922 memory_bytes: 8 * GI,
923 ephemeral_storage_bytes: 20 * GI,
924 gpu: None,
925 },
926 InstanceTypeSpec {
927 name: "Standard_B4ms",
928 platform: Platform::Azure,
929 family: InstanceFamily::Burstable,
930 architecture: Architecture::X86_64,
931 vcpu: 4,
932 memory_bytes: 16 * GI,
933 ephemeral_storage_bytes: 20 * GI,
934 gpu: None,
935 },
936 InstanceTypeSpec {
938 name: "Standard_D2s_v5",
939 platform: Platform::Azure,
940 family: InstanceFamily::GeneralPurpose,
941 architecture: Architecture::X86_64,
942 vcpu: 2,
943 memory_bytes: 8 * GI,
944 ephemeral_storage_bytes: 20 * GI,
945 gpu: None,
946 },
947 InstanceTypeSpec {
948 name: "Standard_D4s_v5",
949 platform: Platform::Azure,
950 family: InstanceFamily::GeneralPurpose,
951 architecture: Architecture::X86_64,
952 vcpu: 4,
953 memory_bytes: 16 * GI,
954 ephemeral_storage_bytes: 20 * GI,
955 gpu: None,
956 },
957 InstanceTypeSpec {
958 name: "Standard_D8s_v5",
959 platform: Platform::Azure,
960 family: InstanceFamily::GeneralPurpose,
961 architecture: Architecture::X86_64,
962 vcpu: 8,
963 memory_bytes: 32 * GI,
964 ephemeral_storage_bytes: 20 * GI,
965 gpu: None,
966 },
967 InstanceTypeSpec {
968 name: "Standard_D16s_v5",
969 platform: Platform::Azure,
970 family: InstanceFamily::GeneralPurpose,
971 architecture: Architecture::X86_64,
972 vcpu: 16,
973 memory_bytes: 64 * GI,
974 ephemeral_storage_bytes: 20 * GI,
975 gpu: None,
976 },
977 InstanceTypeSpec {
979 name: "Standard_F2s_v2",
980 platform: Platform::Azure,
981 family: InstanceFamily::ComputeOptimized,
982 architecture: Architecture::X86_64,
983 vcpu: 2,
984 memory_bytes: 4 * GI,
985 ephemeral_storage_bytes: 20 * GI,
986 gpu: None,
987 },
988 InstanceTypeSpec {
989 name: "Standard_F4s_v2",
990 platform: Platform::Azure,
991 family: InstanceFamily::ComputeOptimized,
992 architecture: Architecture::X86_64,
993 vcpu: 4,
994 memory_bytes: 8 * GI,
995 ephemeral_storage_bytes: 20 * GI,
996 gpu: None,
997 },
998 InstanceTypeSpec {
999 name: "Standard_F8s_v2",
1000 platform: Platform::Azure,
1001 family: InstanceFamily::ComputeOptimized,
1002 architecture: Architecture::X86_64,
1003 vcpu: 8,
1004 memory_bytes: 16 * GI,
1005 ephemeral_storage_bytes: 20 * GI,
1006 gpu: None,
1007 },
1008 InstanceTypeSpec {
1009 name: "Standard_F16s_v2",
1010 platform: Platform::Azure,
1011 family: InstanceFamily::ComputeOptimized,
1012 architecture: Architecture::X86_64,
1013 vcpu: 16,
1014 memory_bytes: 32 * GI,
1015 ephemeral_storage_bytes: 20 * GI,
1016 gpu: None,
1017 },
1018 InstanceTypeSpec {
1020 name: "Standard_E2s_v5",
1021 platform: Platform::Azure,
1022 family: InstanceFamily::MemoryOptimized,
1023 architecture: Architecture::X86_64,
1024 vcpu: 2,
1025 memory_bytes: 16 * GI,
1026 ephemeral_storage_bytes: 20 * GI,
1027 gpu: None,
1028 },
1029 InstanceTypeSpec {
1030 name: "Standard_E4s_v5",
1031 platform: Platform::Azure,
1032 family: InstanceFamily::MemoryOptimized,
1033 architecture: Architecture::X86_64,
1034 vcpu: 4,
1035 memory_bytes: 32 * GI,
1036 ephemeral_storage_bytes: 20 * GI,
1037 gpu: None,
1038 },
1039 InstanceTypeSpec {
1040 name: "Standard_E8s_v5",
1041 platform: Platform::Azure,
1042 family: InstanceFamily::MemoryOptimized,
1043 architecture: Architecture::X86_64,
1044 vcpu: 8,
1045 memory_bytes: 64 * GI,
1046 ephemeral_storage_bytes: 20 * GI,
1047 gpu: None,
1048 },
1049 InstanceTypeSpec {
1050 name: "Standard_E16s_v5",
1051 platform: Platform::Azure,
1052 family: InstanceFamily::MemoryOptimized,
1053 architecture: Architecture::X86_64,
1054 vcpu: 16,
1055 memory_bytes: 128 * GI,
1056 ephemeral_storage_bytes: 20 * GI,
1057 gpu: None,
1058 },
1059 InstanceTypeSpec {
1061 name: "Standard_L8s_v3",
1062 platform: Platform::Azure,
1063 family: InstanceFamily::StorageOptimized,
1064 architecture: Architecture::X86_64,
1065 vcpu: 8,
1066 memory_bytes: 64 * GI,
1067 ephemeral_storage_bytes: 1788 * GI,
1068 gpu: None,
1069 },
1070 InstanceTypeSpec {
1071 name: "Standard_L16s_v3",
1072 platform: Platform::Azure,
1073 family: InstanceFamily::StorageOptimized,
1074 architecture: Architecture::X86_64,
1075 vcpu: 16,
1076 memory_bytes: 128 * GI,
1077 ephemeral_storage_bytes: 3576 * GI,
1078 gpu: None,
1079 },
1080 InstanceTypeSpec {
1081 name: "Standard_L32s_v3",
1082 platform: Platform::Azure,
1083 family: InstanceFamily::StorageOptimized,
1084 architecture: Architecture::X86_64,
1085 vcpu: 32,
1086 memory_bytes: 256 * GI,
1087 ephemeral_storage_bytes: 7154 * GI,
1088 gpu: None,
1089 },
1090 InstanceTypeSpec {
1092 name: "Standard_NC4as_T4_v3",
1093 platform: Platform::Azure,
1094 family: InstanceFamily::GpuCompute,
1095 architecture: Architecture::X86_64,
1096 vcpu: 4,
1097 memory_bytes: 28 * GI,
1098 ephemeral_storage_bytes: 176 * GI,
1099 gpu: Some(CatalogGpu {
1100 gpu_type: "nvidia-t4",
1101 count: 1,
1102 }),
1103 },
1104 InstanceTypeSpec {
1106 name: "Standard_NC24ads_A100_v4",
1107 platform: Platform::Azure,
1108 family: InstanceFamily::GpuCompute,
1109 architecture: Architecture::X86_64,
1110 vcpu: 24,
1111 memory_bytes: 220 * GI,
1112 ephemeral_storage_bytes: 958 * GI,
1113 gpu: Some(CatalogGpu {
1114 gpu_type: "nvidia-a100",
1115 count: 1,
1116 }),
1117 },
1118 InstanceTypeSpec {
1119 name: "Standard_NC96ads_A100_v4",
1120 platform: Platform::Azure,
1121 family: InstanceFamily::GpuCompute,
1122 architecture: Architecture::X86_64,
1123 vcpu: 96,
1124 memory_bytes: 880 * GI,
1125 ephemeral_storage_bytes: 3916 * GI,
1126 gpu: Some(CatalogGpu {
1127 gpu_type: "nvidia-a100",
1128 count: 4,
1129 }),
1130 },
1131 InstanceTypeSpec {
1133 name: "Standard_ND96isr_H100_v5",
1134 platform: Platform::Azure,
1135 family: InstanceFamily::GpuCompute,
1136 architecture: Architecture::X86_64,
1137 vcpu: 96,
1138 memory_bytes: 1900 * GI,
1139 ephemeral_storage_bytes: 1000 * GI,
1140 gpu: Some(CatalogGpu {
1141 gpu_type: "nvidia-h100",
1142 count: 8,
1143 }),
1144 },
1145];
1146
1147pub fn catalog_for_platform(platform: Platform) -> Vec<&'static InstanceTypeSpec> {
1153 CATALOG
1154 .iter()
1155 .filter(|spec| spec.platform == platform)
1156 .collect()
1157}
1158
1159pub fn find_instance_type(platform: Platform, name: &str) -> Option<&'static InstanceTypeSpec> {
1161 CATALOG
1162 .iter()
1163 .find(|spec| spec.platform == platform && spec.name == name)
1164}
1165
1166#[derive(Debug, Clone)]
1172pub struct WorkloadRequirements {
1173 pub total_cpu_at_desired: f64,
1175 pub total_memory_bytes_at_desired: u64,
1177 pub total_cpu_at_max: f64,
1179 pub total_memory_bytes_at_max: u64,
1181 pub max_cpu_per_container: f64,
1183 pub max_memory_per_container: u64,
1185 pub max_ephemeral_storage_bytes: u64,
1187 pub gpu: Option<GpuSpec>,
1189 pub architecture: Option<Architecture>,
1191 pub nested_virt: bool,
1195}
1196
1197#[derive(Debug, Clone)]
1199pub struct InstanceSelection {
1200 pub instance_type: &'static str,
1202 pub profile: MachineProfile,
1204 pub min_machines: u32,
1206 pub max_machines: u32,
1208}
1209
1210const STORAGE_OPTIMIZED_THRESHOLD: u64 = 200 * GI;
1212
1213const MAX_MACHINES_PER_CLUSTER: u32 = 10;
1215
1216const MAX_STANDARD_VCPU: u32 = 8;
1219
1220const STANDARD_CONTAINERS_PER_MACHINE: f64 = 2.0;
1222
1223const OVERHEAD_FACTOR: f64 = 1.25;
1225
1226const SYSTEM_RESERVE_CPU: f64 = 0.5;
1228
1229const WORKLOAD_HEADROOM_FACTOR: f64 = 1.15;
1231
1232pub fn select_instance_type(
1243 platform: Platform,
1244 requirements: &WorkloadRequirements,
1245) -> Result<InstanceSelection, String> {
1246 let raw_family = select_family(requirements);
1251 let family = if requirements.nested_virt && raw_family == InstanceFamily::Burstable {
1252 InstanceFamily::GeneralPurpose
1253 } else {
1254 raw_family
1255 };
1256
1257 let candidates: Vec<&InstanceTypeSpec> = CATALOG
1258 .iter()
1259 .filter(|spec| spec.platform == platform && spec.family == family)
1260 .filter(|spec| {
1261 if requirements.nested_virt {
1262 spec.is_nested_virt_capable()
1263 } else {
1264 !spec.is_nested_virt_capable()
1265 }
1266 })
1267 .collect();
1268
1269 if candidates.is_empty() {
1270 return Err(if requirements.nested_virt {
1271 format!(
1272 "no nested-virt-capable {family:?} instance types in catalog for platform {platform}; \
1273 only 8th-gen Intel families (m8i/c8i/r8i) support nested virtualization on AWS"
1274 )
1275 } else {
1276 format!("no {family:?} instance types in catalog for platform {platform}")
1277 });
1278 }
1279
1280 let candidates = if let Some(ref gpu) = requirements.gpu {
1282 let filtered: Vec<&InstanceTypeSpec> = candidates
1283 .into_iter()
1284 .filter(|spec| {
1285 spec.gpu.as_ref().map_or(false, |g| {
1286 g.gpu_type == gpu.gpu_type && g.count >= gpu.count
1287 })
1288 })
1289 .collect();
1290 if filtered.is_empty() {
1291 return Err(format!(
1292 "no instance type for GPU type '{}' x{} on platform {platform}",
1293 gpu.gpu_type, gpu.count
1294 ));
1295 }
1296 filtered
1297 } else {
1298 candidates
1299 };
1300
1301 let candidates = if family == InstanceFamily::StorageOptimized {
1303 let filtered: Vec<&InstanceTypeSpec> = candidates
1304 .into_iter()
1305 .filter(|spec| spec.ephemeral_storage_bytes >= requirements.max_ephemeral_storage_bytes)
1306 .collect();
1307 if filtered.is_empty() {
1308 return Err(format!(
1309 "no storage-optimized instance with >= {} bytes ephemeral storage on platform {platform}",
1310 requirements.max_ephemeral_storage_bytes
1311 ));
1312 }
1313 filtered
1314 } else {
1315 candidates
1316 };
1317
1318 let architecture = requirements
1319 .architecture
1320 .or_else(|| default_architecture(platform))
1321 .ok_or_else(|| format!("platform {platform} has no default compute architecture"))?;
1322 let candidates: Vec<&InstanceTypeSpec> = candidates
1323 .into_iter()
1324 .filter(|spec| spec.architecture == architecture)
1325 .collect();
1326 if candidates.is_empty() {
1327 return Err(format!(
1328 "architecture {architecture:?} is unavailable for this workload on platform {platform}"
1329 ));
1330 }
1331
1332 let vcpu_cap =
1334 if family == InstanceFamily::GpuCompute || family == InstanceFamily::StorageOptimized {
1335 u32::MAX
1336 } else {
1337 MAX_STANDARD_VCPU
1338 };
1339
1340 let desired_target_machines = desired_target_machines(requirements);
1341 let target_cpu =
1342 (requirements.max_cpu_per_container * STANDARD_CONTAINERS_PER_MACHINE * OVERHEAD_FACTOR)
1343 .max(
1344 requirements.total_cpu_at_desired * WORKLOAD_HEADROOM_FACTOR
1345 / desired_target_machines as f64,
1346 )
1347 .max(0.25);
1348 let target_memory = (requirements.max_memory_per_container as f64
1349 * STANDARD_CONTAINERS_PER_MACHINE
1350 * OVERHEAD_FACTOR)
1351 .max(
1352 requirements.total_memory_bytes_at_desired as f64 * WORKLOAD_HEADROOM_FACTOR
1353 / desired_target_machines as f64,
1354 )
1355 .max(256.0 * MI as f64);
1356
1357 let selected = candidates
1359 .iter()
1360 .filter(|spec| {
1361 spec.vcpu <= vcpu_cap
1362 && spec.vcpu as f64 >= target_cpu
1363 && spec.memory_bytes as f64 >= target_memory
1364 })
1365 .min_by_key(|spec| spec.vcpu)
1366 .or_else(|| {
1367 candidates
1369 .iter()
1370 .filter(|spec| spec.vcpu <= vcpu_cap)
1371 .max_by_key(|spec| spec.vcpu)
1372 })
1373 .or_else(|| {
1374 candidates.iter().min_by_key(|spec| spec.vcpu)
1376 })
1377 .ok_or_else(|| format!("no instance types available for platform {platform}"))?;
1378
1379 let max_machines = compute_max_machines(requirements, selected);
1381 let min_machines = compute_min_machines(requirements, selected, max_machines);
1382
1383 Ok(InstanceSelection {
1384 instance_type: selected.name,
1385 profile: selected.to_machine_profile(),
1386 min_machines,
1387 max_machines,
1388 })
1389}
1390
1391pub fn select_family(requirements: &WorkloadRequirements) -> InstanceFamily {
1397 if requirements.gpu.is_some() {
1399 return InstanceFamily::GpuCompute;
1400 }
1401
1402 if requirements.max_ephemeral_storage_bytes > STORAGE_OPTIMIZED_THRESHOLD {
1404 return InstanceFamily::StorageOptimized;
1405 }
1406
1407 if requirements.total_cpu_at_max < 2.0 {
1409 return InstanceFamily::Burstable;
1410 }
1411
1412 InstanceFamily::GeneralPurpose
1414}
1415
1416fn compute_max_machines(requirements: &WorkloadRequirements, instance: &InstanceTypeSpec) -> u32 {
1418 let cpu_with_headroom = requirements.total_cpu_at_max * WORKLOAD_HEADROOM_FACTOR;
1419 let cpu_machines = (cpu_with_headroom / allocatable_cpu(instance)).ceil() as u32;
1420
1421 let mem_with_headroom =
1422 requirements.total_memory_bytes_at_max as f64 * WORKLOAD_HEADROOM_FACTOR;
1423 let mem_machines =
1424 (mem_with_headroom / allocatable_memory_bytes(instance) as f64).ceil() as u32;
1425
1426 cpu_machines
1428 .max(mem_machines)
1429 .max(1)
1430 .min(MAX_MACHINES_PER_CLUSTER)
1431}
1432
1433fn compute_min_machines(
1435 requirements: &WorkloadRequirements,
1436 instance: &InstanceTypeSpec,
1437 max_machines: u32,
1438) -> u32 {
1439 let cpu_with_headroom = requirements.total_cpu_at_desired * WORKLOAD_HEADROOM_FACTOR;
1440 let cpu_machines = (cpu_with_headroom / allocatable_cpu(instance)).ceil() as u32;
1441
1442 let mem_with_headroom =
1443 requirements.total_memory_bytes_at_desired as f64 * WORKLOAD_HEADROOM_FACTOR;
1444 let mem_machines =
1445 (mem_with_headroom / allocatable_memory_bytes(instance) as f64).ceil() as u32;
1446
1447 cpu_machines
1448 .max(mem_machines)
1449 .max(1)
1450 .min(2)
1451 .min(max_machines)
1452}
1453
1454fn desired_target_machines(requirements: &WorkloadRequirements) -> u32 {
1455 if requirements.total_cpu_at_desired >= 2.0
1456 || requirements.total_memory_bytes_at_desired >= 4 * GI
1457 {
1458 2
1459 } else {
1460 1
1461 }
1462}
1463
1464fn allocatable_cpu(instance: &InstanceTypeSpec) -> f64 {
1465 (instance.vcpu as f64 - SYSTEM_RESERVE_CPU).max(0.25)
1466}
1467
1468fn allocatable_memory_bytes(instance: &InstanceTypeSpec) -> u64 {
1469 instance
1470 .memory_bytes
1471 .saturating_sub(system_reserve_memory_bytes(instance.memory_bytes))
1472 .max(256 * MI)
1473}
1474
1475fn system_reserve_memory_bytes(memory_bytes: u64) -> u64 {
1476 if memory_bytes < 4 * GI {
1477 256 * MI
1478 } else if memory_bytes < 16 * GI {
1479 512 * MI
1480 } else {
1481 GI
1482 }
1483}
1484
1485#[cfg(test)]
1490mod tests {
1491 use super::*;
1492 use crate::BinaryTarget;
1493
1494 #[test]
1497 fn test_parse_cpu_plain() {
1498 assert_eq!(parse_cpu("1").unwrap(), 1.0);
1499 assert_eq!(parse_cpu("0.5").unwrap(), 0.5);
1500 assert_eq!(parse_cpu("2.0").unwrap(), 2.0);
1501 assert_eq!(parse_cpu("16").unwrap(), 16.0);
1502 }
1503
1504 #[test]
1505 fn test_parse_cpu_millicore() {
1506 assert_eq!(parse_cpu("500m").unwrap(), 0.5);
1507 assert_eq!(parse_cpu("250m").unwrap(), 0.25);
1508 assert_eq!(parse_cpu("1000m").unwrap(), 1.0);
1509 assert_eq!(parse_cpu("100m").unwrap(), 0.1);
1510 }
1511
1512 #[test]
1513 fn test_parse_cpu_invalid() {
1514 assert!(parse_cpu("").is_err());
1515 assert!(parse_cpu("abc").is_err());
1516 assert!(parse_cpu("m").is_err());
1517 }
1518
1519 #[test]
1520 fn test_parse_memory_binary_suffixes() {
1521 assert_eq!(parse_memory_bytes("1Ki").unwrap(), 1024);
1522 assert_eq!(parse_memory_bytes("1Mi").unwrap(), 1024 * 1024);
1523 assert_eq!(parse_memory_bytes("1Gi").unwrap(), 1024 * 1024 * 1024);
1524 assert_eq!(parse_memory_bytes("4Gi").unwrap(), 4 * 1024 * 1024 * 1024);
1525 assert_eq!(parse_memory_bytes("512Mi").unwrap(), 512 * 1024 * 1024);
1526 assert_eq!(
1527 parse_memory_bytes("1Ti").unwrap(),
1528 1024u64 * 1024 * 1024 * 1024
1529 );
1530 }
1531
1532 #[test]
1533 fn test_parse_memory_decimal_suffixes() {
1534 assert_eq!(parse_memory_bytes("1k").unwrap(), 1000);
1535 assert_eq!(parse_memory_bytes("1M").unwrap(), 1_000_000);
1536 assert_eq!(parse_memory_bytes("1G").unwrap(), 1_000_000_000);
1537 assert_eq!(parse_memory_bytes("1T").unwrap(), 1_000_000_000_000);
1538 }
1539
1540 #[test]
1541 fn test_parse_memory_plain_bytes() {
1542 assert_eq!(parse_memory_bytes("1024").unwrap(), 1024);
1543 assert_eq!(parse_memory_bytes("0").unwrap(), 0);
1544 }
1545
1546 #[test]
1547 fn test_parse_memory_invalid() {
1548 assert!(parse_memory_bytes("").is_err());
1549 assert!(parse_memory_bytes("abc").is_err());
1550 assert!(parse_memory_bytes("Gi").is_err());
1551 }
1552
1553 #[test]
1554 fn test_parse_memory_fractional() {
1555 assert_eq!(parse_memory_bytes("0.5Gi").unwrap(), GI / 2);
1556 assert_eq!(parse_memory_bytes("1.5Gi").unwrap(), GI + GI / 2);
1557 }
1558
1559 #[test]
1562 fn test_catalog_has_entries_for_all_cloud_platforms() {
1563 assert!(!catalog_for_platform(Platform::Aws).is_empty());
1564 assert!(!catalog_for_platform(Platform::Gcp).is_empty());
1565 assert!(!catalog_for_platform(Platform::Azure).is_empty());
1566 }
1567
1568 #[test]
1569 fn test_catalog_no_entries_for_non_cloud_platforms() {
1570 assert!(catalog_for_platform(Platform::Local).is_empty());
1571 assert!(catalog_for_platform(Platform::Kubernetes).is_empty());
1572 }
1573
1574 #[test]
1575 fn test_find_known_instance_type() {
1576 let spec =
1577 find_instance_type(Platform::Aws, "m7g.2xlarge").expect("should find m7g.2xlarge");
1578 assert_eq!(spec.vcpu, 8);
1579 assert_eq!(spec.memory_bytes, 32 * GI);
1580 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1581 }
1582
1583 #[test]
1584 fn test_find_aws_c8i_nested_virt_instance_type() {
1585 let spec = find_instance_type(Platform::Aws, "c8i.large").expect("should find c8i.large");
1586 assert_eq!(spec.vcpu, 2);
1587 assert_eq!(spec.memory_bytes, 4 * GI);
1588 assert_eq!(spec.family, InstanceFamily::ComputeOptimized);
1589 assert_eq!(spec.architecture, Architecture::X86_64);
1590 assert!(spec.is_nested_virt_capable());
1591 }
1592
1593 #[test]
1594 fn test_find_unknown_instance_type() {
1595 assert!(find_instance_type(Platform::Aws, "nonexistent.xlarge").is_none());
1596 }
1597
1598 #[test]
1599 fn test_find_wrong_platform() {
1600 assert!(find_instance_type(Platform::Gcp, "m7g.2xlarge").is_none());
1601 }
1602
1603 #[test]
1604 fn test_to_machine_profile() {
1605 let spec = find_instance_type(Platform::Aws, "m7g.2xlarge").unwrap();
1606 let profile = spec.to_machine_profile();
1607 assert_eq!(profile.cpu, "8.0");
1608 assert_eq!(profile.memory_bytes, 32 * GI);
1609 assert_eq!(profile.ephemeral_storage_bytes, 20 * GI);
1610 assert!(profile.gpu.is_none());
1611 }
1612
1613 #[test]
1614 fn test_to_machine_profile_with_gpu() {
1615 let spec = find_instance_type(Platform::Aws, "p4d.24xlarge").unwrap();
1616 let profile = spec.to_machine_profile();
1617 let gpu = profile.gpu.as_ref().expect("should have GPU");
1618 assert_eq!(gpu.gpu_type, "nvidia-a100");
1619 assert_eq!(gpu.count, 8);
1620 }
1621
1622 #[test]
1625 fn test_select_burstable_for_small_workload() {
1626 let req = WorkloadRequirements {
1627 total_cpu_at_desired: 1.0,
1628 total_memory_bytes_at_desired: 2 * GI,
1629 total_cpu_at_max: 1.0,
1630 total_memory_bytes_at_max: 2 * GI,
1631 max_cpu_per_container: 0.5,
1632 max_memory_per_container: 1 * GI,
1633 max_ephemeral_storage_bytes: 10 * GI,
1634 gpu: None,
1635 architecture: None,
1636 nested_virt: false,
1637 };
1638 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1639 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1640 assert_eq!(spec.family, InstanceFamily::Burstable);
1641 }
1642
1643 #[test]
1644 fn test_select_general_purpose_for_standard_workload() {
1645 let req = WorkloadRequirements {
1647 total_cpu_at_desired: 20.0,
1648 total_memory_bytes_at_desired: 80 * GI,
1649 total_cpu_at_max: 20.0,
1650 total_memory_bytes_at_max: 80 * GI,
1651 max_cpu_per_container: 2.0,
1652 max_memory_per_container: 8 * GI,
1653 max_ephemeral_storage_bytes: 10 * GI,
1654 gpu: None,
1655 architecture: None,
1656 nested_virt: false,
1657 };
1658 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1659 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1660 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1661 }
1662
1663 #[test]
1664 fn test_select_general_purpose_even_for_cpu_heavy() {
1665 let req = WorkloadRequirements {
1667 total_cpu_at_desired: 20.0,
1668 total_memory_bytes_at_desired: 20 * GI,
1669 total_cpu_at_max: 20.0,
1670 total_memory_bytes_at_max: 20 * GI,
1671 max_cpu_per_container: 2.0,
1672 max_memory_per_container: 2 * GI,
1673 max_ephemeral_storage_bytes: 10 * GI,
1674 gpu: None,
1675 architecture: None,
1676 nested_virt: false,
1677 };
1678 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1679 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1680 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1681 }
1682
1683 #[test]
1684 fn test_select_storage_optimized_for_large_ephemeral() {
1685 let req = WorkloadRequirements {
1686 total_cpu_at_desired: 8.0,
1687 total_memory_bytes_at_desired: 32 * GI,
1688 total_cpu_at_max: 8.0,
1689 total_memory_bytes_at_max: 32 * GI,
1690 max_cpu_per_container: 2.0,
1691 max_memory_per_container: 8 * GI,
1692 max_ephemeral_storage_bytes: 500 * GI,
1693 gpu: None,
1694 architecture: Some(Architecture::X86_64),
1695 nested_virt: false,
1696 };
1697 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1698 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1699 assert_eq!(spec.family, InstanceFamily::StorageOptimized);
1700 }
1701
1702 #[test]
1703 fn test_select_gpu_instance() {
1704 let req = WorkloadRequirements {
1705 total_cpu_at_desired: 8.0,
1706 total_memory_bytes_at_desired: 32 * GI,
1707 total_cpu_at_max: 8.0,
1708 total_memory_bytes_at_max: 32 * GI,
1709 max_cpu_per_container: 4.0,
1710 max_memory_per_container: 16 * GI,
1711 max_ephemeral_storage_bytes: 10 * GI,
1712 gpu: Some(GpuSpec {
1713 gpu_type: "nvidia-a100".to_string(),
1714 count: 1,
1715 }),
1716 architecture: Some(Architecture::X86_64),
1717 nested_virt: false,
1718 };
1719 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1720 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1721 assert_eq!(spec.family, InstanceFamily::GpuCompute);
1722 assert!(spec.gpu.is_some());
1723 }
1724
1725 #[test]
1726 fn test_select_uses_each_cloud_image_target_architecture() {
1727 let req = WorkloadRequirements {
1728 total_cpu_at_desired: 4.0,
1729 total_memory_bytes_at_desired: 16 * GI,
1730 total_cpu_at_max: 4.0,
1731 total_memory_bytes_at_max: 16 * GI,
1732 max_cpu_per_container: 1.0,
1733 max_memory_per_container: 4 * GI,
1734 max_ephemeral_storage_bytes: 10 * GI,
1735 gpu: None,
1736 architecture: None,
1737 nested_virt: false,
1738 };
1739 for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
1740 let sel = select_instance_type(platform, &req)
1741 .unwrap_or_else(|error| panic!("selection failed for {platform}: {error}"));
1742 let spec = find_instance_type(platform, sel.instance_type)
1743 .expect("selected machine should exist in the catalog");
1744 assert_eq!(
1745 Some(spec.architecture),
1746 default_architecture(platform),
1747 "machine architecture must match the image target for {platform}"
1748 );
1749 }
1750 }
1751
1752 #[test]
1753 fn test_machine_count_reasonable() {
1754 let req = WorkloadRequirements {
1756 total_cpu_at_desired: 20.0,
1757 total_memory_bytes_at_desired: 40 * GI,
1758 total_cpu_at_max: 20.0,
1759 total_memory_bytes_at_max: 40 * GI,
1760 max_cpu_per_container: 1.0,
1761 max_memory_per_container: 2 * GI,
1762 max_ephemeral_storage_bytes: 10 * GI,
1763 gpu: None,
1764 architecture: None,
1765 nested_virt: false,
1766 };
1767 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1768 assert!(sel.min_machines >= 1);
1769 assert!(sel.max_machines <= MAX_MACHINES_PER_CLUSTER);
1770 assert!(sel.max_machines >= sel.min_machines);
1771 }
1772
1773 #[test]
1774 fn test_instance_size_capped_at_8_vcpu() {
1775 let req = WorkloadRequirements {
1777 total_cpu_at_desired: 70.0,
1778 total_memory_bytes_at_desired: 140 * GI,
1779 total_cpu_at_max: 70.0,
1780 total_memory_bytes_at_max: 140 * GI,
1781 max_cpu_per_container: 2.0,
1782 max_memory_per_container: 4 * GI,
1783 max_ephemeral_storage_bytes: 10 * GI,
1784 gpu: None,
1785 architecture: None,
1786 nested_virt: false,
1787 };
1788 let sel = select_instance_type(Platform::Gcp, &req).unwrap();
1789 let spec = find_instance_type(Platform::Gcp, sel.instance_type).unwrap();
1790 assert!(
1791 spec.vcpu <= MAX_STANDARD_VCPU,
1792 "selected {} with {} vCPUs, expected <= {}",
1793 spec.name,
1794 spec.vcpu,
1795 MAX_STANDARD_VCPU
1796 );
1797 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1798 assert!(sel.max_machines > 1);
1800 }
1801
1802 #[test]
1803 fn test_larger_autoscaled_workload_gets_reasonable_instance() {
1804 let req = WorkloadRequirements {
1807 total_cpu_at_desired: 70.0,
1808 total_memory_bytes_at_desired: 140 * GI,
1809 total_cpu_at_max: 70.0, total_memory_bytes_at_max: 140 * GI, max_cpu_per_container: 2.0,
1812 max_memory_per_container: 4 * GI,
1813 max_ephemeral_storage_bytes: 20 * GI,
1814 gpu: None,
1815 architecture: None,
1816 nested_virt: false,
1817 };
1818 let sel = select_instance_type(Platform::Gcp, &req).unwrap();
1819 assert_eq!(sel.instance_type, "n2-standard-8");
1821 assert!(sel.max_machines >= 2);
1822 }
1823
1824 #[test]
1831 fn test_select_aws_picks_m8i_when_nested_virt_required() {
1832 let req = WorkloadRequirements {
1833 total_cpu_at_desired: 4.0,
1834 total_memory_bytes_at_desired: 8 * GI,
1835 total_cpu_at_max: 4.0,
1836 total_memory_bytes_at_max: 8 * GI,
1837 max_cpu_per_container: 4.0,
1838 max_memory_per_container: 8 * GI,
1839 max_ephemeral_storage_bytes: 10 * GI,
1840 gpu: None,
1841 architecture: Some(Architecture::X86_64),
1842 nested_virt: true,
1843 };
1844 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1845 assert!(
1846 sel.instance_type.starts_with("m8i.")
1847 || sel.instance_type.starts_with("c8i.")
1848 || sel.instance_type.starts_with("r8i."),
1849 "expected an m8i/c8i/r8i instance, got {}",
1850 sel.instance_type
1851 );
1852 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1853 assert!(spec.is_nested_virt_capable());
1854 }
1855
1856 #[test]
1857 fn test_select_aws_defaults_to_image_target_architecture() {
1858 let req = WorkloadRequirements {
1859 total_cpu_at_desired: 4.0,
1860 total_memory_bytes_at_desired: 8 * GI,
1861 total_cpu_at_max: 4.0,
1862 total_memory_bytes_at_max: 8 * GI,
1863 max_cpu_per_container: 4.0,
1864 max_memory_per_container: 8 * GI,
1865 max_ephemeral_storage_bytes: 10 * GI,
1866 gpu: None,
1867 architecture: None,
1868 nested_virt: false,
1869 };
1870 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1871 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1872 assert_eq!(spec.architecture, Architecture::Arm64);
1873 }
1874
1875 #[test]
1876 fn test_cloud_defaults_match_image_target_architectures() {
1877 for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
1878 let target = BinaryTarget::defaults_for_platform(platform)
1879 .into_iter()
1880 .next()
1881 .expect("managed cloud should have a default image target");
1882 let image_architecture = match target.oci_arch() {
1883 "arm64" => Architecture::Arm64,
1884 "amd64" => Architecture::X86_64,
1885 architecture => {
1886 panic!("unsupported managed-cloud image architecture {architecture}")
1887 }
1888 };
1889
1890 assert_eq!(default_architecture(platform), Some(image_architecture));
1891 }
1892 }
1893
1894 #[test]
1896 fn test_select_aws_uses_graviton_for_explicit_arm64() {
1897 let req = WorkloadRequirements {
1898 total_cpu_at_desired: 4.0,
1899 total_memory_bytes_at_desired: 8 * GI,
1900 total_cpu_at_max: 4.0,
1901 total_memory_bytes_at_max: 8 * GI,
1902 max_cpu_per_container: 4.0,
1903 max_memory_per_container: 8 * GI,
1904 max_ephemeral_storage_bytes: 10 * GI,
1905 gpu: None,
1906 architecture: Some(Architecture::Arm64),
1907 nested_virt: false,
1908 };
1909 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1910 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1911 assert_eq!(spec.architecture, Architecture::Arm64);
1912 }
1913
1914 #[test]
1915 fn test_select_rejects_explicit_architecture_missing_from_cloud_catalog() {
1916 let req = WorkloadRequirements {
1917 total_cpu_at_desired: 1.0,
1918 total_memory_bytes_at_desired: 2 * GI,
1919 total_cpu_at_max: 1.0,
1920 total_memory_bytes_at_max: 2 * GI,
1921 max_cpu_per_container: 1.0,
1922 max_memory_per_container: 2 * GI,
1923 max_ephemeral_storage_bytes: 10 * GI,
1924 gpu: None,
1925 architecture: Some(Architecture::Arm64),
1926 nested_virt: false,
1927 };
1928
1929 let error = select_instance_type(Platform::Gcp, &req)
1930 .expect_err("GCP catalog has no ARM64 machine");
1931
1932 assert!(error.contains("architecture Arm64 is unavailable"));
1933 }
1934
1935 #[test]
1936 fn test_profile_has_required_fields() {
1937 let req = WorkloadRequirements {
1938 total_cpu_at_desired: 4.0,
1939 total_memory_bytes_at_desired: 16 * GI,
1940 total_cpu_at_max: 4.0,
1941 total_memory_bytes_at_max: 16 * GI,
1942 max_cpu_per_container: 1.0,
1943 max_memory_per_container: 4 * GI,
1944 max_ephemeral_storage_bytes: 10 * GI,
1945 gpu: None,
1946 architecture: None,
1947 nested_virt: false,
1948 };
1949 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1950 assert!(!sel.profile.cpu.is_empty());
1951 assert!(sel.profile.memory_bytes > 0);
1952 assert!(sel.profile.ephemeral_storage_bytes > 0);
1953 }
1954
1955 #[test]
1956 fn test_error_for_unsupported_gpu_type() {
1957 let req = WorkloadRequirements {
1958 total_cpu_at_desired: 8.0,
1959 total_memory_bytes_at_desired: 32 * GI,
1960 total_cpu_at_max: 8.0,
1961 total_memory_bytes_at_max: 32 * GI,
1962 max_cpu_per_container: 4.0,
1963 max_memory_per_container: 16 * GI,
1964 max_ephemeral_storage_bytes: 10 * GI,
1965 gpu: Some(GpuSpec {
1966 gpu_type: "amd-mi300".to_string(),
1967 count: 1,
1968 }),
1969 architecture: None,
1970 nested_virt: false,
1971 };
1972 let result = select_instance_type(Platform::Aws, &req);
1973 assert!(result.is_err());
1974 }
1975
1976 #[test]
1977 fn test_catalog_instance_types_sorted_by_vcpu_within_family() {
1978 for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
1981 let entries = catalog_for_platform(platform);
1982 let mut by_family: std::collections::HashMap<_, Vec<_>> =
1983 std::collections::HashMap::new();
1984 for entry in entries {
1985 by_family
1986 .entry(format!("{:?}", entry.family))
1987 .or_default()
1988 .push(entry);
1989 }
1990 for (family, instances) in &by_family {
1991 for window in instances.windows(2) {
1992 assert!(
1993 window[0].vcpu <= window[1].vcpu,
1994 "catalog not sorted by vcpu for {platform}/{family}: {} ({}) > {} ({})",
1995 window[0].name,
1996 window[0].vcpu,
1997 window[1].name,
1998 window[1].vcpu
1999 );
2000 }
2001 }
2002 }
2003 }
2004}