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 SYSTEM_RESERVE_CPU: f64 = 0.5;
1222
1223const WORKLOAD_HEADROOM_FACTOR: f64 = 1.15;
1225
1226pub fn select_instance_type(
1237 platform: Platform,
1238 requirements: &WorkloadRequirements,
1239) -> Result<InstanceSelection, String> {
1240 let raw_family = select_family(requirements);
1245 let family = if requirements.nested_virt && raw_family == InstanceFamily::Burstable {
1246 InstanceFamily::GeneralPurpose
1247 } else {
1248 raw_family
1249 };
1250
1251 let candidates: Vec<&InstanceTypeSpec> = CATALOG
1252 .iter()
1253 .filter(|spec| spec.platform == platform && spec.family == family)
1254 .filter(|spec| {
1255 if requirements.nested_virt {
1256 spec.is_nested_virt_capable()
1257 } else {
1258 !spec.is_nested_virt_capable()
1259 }
1260 })
1261 .collect();
1262
1263 if candidates.is_empty() {
1264 return Err(if requirements.nested_virt {
1265 format!(
1266 "no nested-virt-capable {family:?} instance types in catalog for platform {platform}; \
1267 only 8th-gen Intel families (m8i/c8i/r8i) support nested virtualization on AWS"
1268 )
1269 } else {
1270 format!("no {family:?} instance types in catalog for platform {platform}")
1271 });
1272 }
1273
1274 let candidates = if let Some(ref gpu) = requirements.gpu {
1276 let filtered: Vec<&InstanceTypeSpec> = candidates
1277 .into_iter()
1278 .filter(|spec| {
1279 spec.gpu.as_ref().map_or(false, |g| {
1280 g.gpu_type == gpu.gpu_type && g.count >= gpu.count
1281 })
1282 })
1283 .collect();
1284 if filtered.is_empty() {
1285 return Err(format!(
1286 "no instance type for GPU type '{}' x{} on platform {platform}",
1287 gpu.gpu_type, gpu.count
1288 ));
1289 }
1290 filtered
1291 } else {
1292 candidates
1293 };
1294
1295 let candidates = if family == InstanceFamily::StorageOptimized {
1297 let filtered: Vec<&InstanceTypeSpec> = candidates
1298 .into_iter()
1299 .filter(|spec| spec.ephemeral_storage_bytes >= requirements.max_ephemeral_storage_bytes)
1300 .collect();
1301 if filtered.is_empty() {
1302 return Err(format!(
1303 "no storage-optimized instance with >= {} bytes ephemeral storage on platform {platform}",
1304 requirements.max_ephemeral_storage_bytes
1305 ));
1306 }
1307 filtered
1308 } else {
1309 candidates
1310 };
1311
1312 let architecture = requirements
1313 .architecture
1314 .or_else(|| default_architecture(platform))
1315 .ok_or_else(|| format!("platform {platform} has no default compute architecture"))?;
1316 let candidates: Vec<&InstanceTypeSpec> = candidates
1317 .into_iter()
1318 .filter(|spec| spec.architecture == architecture)
1319 .collect();
1320 if candidates.is_empty() {
1321 return Err(format!(
1322 "architecture {architecture:?} is unavailable for this workload on platform {platform}"
1323 ));
1324 }
1325
1326 let vcpu_cap =
1328 if family == InstanceFamily::GpuCompute || family == InstanceFamily::StorageOptimized {
1329 u32::MAX
1330 } else {
1331 MAX_STANDARD_VCPU
1332 };
1333
1334 let desired_target_machines = desired_target_machines(requirements);
1335 let target_cpu = requirements
1336 .max_cpu_per_container
1337 .max(requirements.total_cpu_at_desired / desired_target_machines as f64)
1338 * WORKLOAD_HEADROOM_FACTOR;
1339 let target_memory = (requirements.max_memory_per_container as f64)
1340 .max(requirements.total_memory_bytes_at_desired as f64 / desired_target_machines as f64)
1341 * WORKLOAD_HEADROOM_FACTOR;
1342
1343 let selected = candidates
1348 .iter()
1349 .filter(|spec| {
1350 spec.vcpu <= vcpu_cap
1351 && allocatable_cpu(spec) >= target_cpu
1352 && allocatable_memory_bytes(spec) as f64 >= target_memory
1353 })
1354 .min_by_key(|spec| spec.vcpu)
1355 .or_else(|| {
1356 candidates
1358 .iter()
1359 .filter(|spec| spec.vcpu <= vcpu_cap)
1360 .max_by_key(|spec| spec.vcpu)
1361 })
1362 .or_else(|| {
1363 candidates.iter().min_by_key(|spec| spec.vcpu)
1365 })
1366 .ok_or_else(|| format!("no instance types available for platform {platform}"))?;
1367
1368 let max_machines = compute_max_machines(requirements, selected);
1370 let min_machines = compute_min_machines(requirements, selected, max_machines);
1371
1372 Ok(InstanceSelection {
1373 instance_type: selected.name,
1374 profile: selected.to_machine_profile(),
1375 min_machines,
1376 max_machines,
1377 })
1378}
1379
1380pub fn select_family(requirements: &WorkloadRequirements) -> InstanceFamily {
1386 if requirements.gpu.is_some() {
1388 return InstanceFamily::GpuCompute;
1389 }
1390
1391 if requirements.max_ephemeral_storage_bytes > STORAGE_OPTIMIZED_THRESHOLD {
1393 return InstanceFamily::StorageOptimized;
1394 }
1395
1396 if requirements.total_cpu_at_max < 2.0 {
1398 return InstanceFamily::Burstable;
1399 }
1400
1401 InstanceFamily::GeneralPurpose
1403}
1404
1405fn compute_max_machines(requirements: &WorkloadRequirements, instance: &InstanceTypeSpec) -> u32 {
1407 let cpu_with_headroom = requirements.total_cpu_at_max * WORKLOAD_HEADROOM_FACTOR;
1408 let cpu_machines = (cpu_with_headroom / allocatable_cpu(instance)).ceil() as u32;
1409
1410 let mem_with_headroom =
1411 requirements.total_memory_bytes_at_max as f64 * WORKLOAD_HEADROOM_FACTOR;
1412 let mem_machines =
1413 (mem_with_headroom / allocatable_memory_bytes(instance) as f64).ceil() as u32;
1414
1415 cpu_machines
1417 .max(mem_machines)
1418 .max(1)
1419 .min(MAX_MACHINES_PER_CLUSTER)
1420}
1421
1422fn compute_min_machines(
1424 requirements: &WorkloadRequirements,
1425 instance: &InstanceTypeSpec,
1426 max_machines: u32,
1427) -> u32 {
1428 let cpu_with_headroom = requirements.total_cpu_at_desired * WORKLOAD_HEADROOM_FACTOR;
1429 let cpu_machines = (cpu_with_headroom / allocatable_cpu(instance)).ceil() as u32;
1430
1431 let mem_with_headroom =
1432 requirements.total_memory_bytes_at_desired as f64 * WORKLOAD_HEADROOM_FACTOR;
1433 let mem_machines =
1434 (mem_with_headroom / allocatable_memory_bytes(instance) as f64).ceil() as u32;
1435
1436 cpu_machines
1437 .max(mem_machines)
1438 .max(1)
1439 .min(2)
1440 .min(max_machines)
1441}
1442
1443fn desired_target_machines(requirements: &WorkloadRequirements) -> u32 {
1444 if requirements.total_cpu_at_desired >= 2.0
1445 || requirements.total_memory_bytes_at_desired >= 4 * GI
1446 {
1447 2
1448 } else {
1449 1
1450 }
1451}
1452
1453fn allocatable_cpu(instance: &InstanceTypeSpec) -> f64 {
1454 (instance.vcpu as f64 - SYSTEM_RESERVE_CPU).max(0.25)
1455}
1456
1457fn allocatable_memory_bytes(instance: &InstanceTypeSpec) -> u64 {
1458 instance
1459 .memory_bytes
1460 .saturating_sub(system_reserve_memory_bytes(instance.memory_bytes))
1461 .max(256 * MI)
1462}
1463
1464fn system_reserve_memory_bytes(memory_bytes: u64) -> u64 {
1465 if memory_bytes < 4 * GI {
1466 256 * MI
1467 } else if memory_bytes < 16 * GI {
1468 512 * MI
1469 } else {
1470 GI
1471 }
1472}
1473
1474#[cfg(test)]
1479mod tests {
1480 use super::*;
1481 use crate::BinaryTarget;
1482
1483 #[test]
1486 fn test_parse_cpu_plain() {
1487 assert_eq!(parse_cpu("1").unwrap(), 1.0);
1488 assert_eq!(parse_cpu("0.5").unwrap(), 0.5);
1489 assert_eq!(parse_cpu("2.0").unwrap(), 2.0);
1490 assert_eq!(parse_cpu("16").unwrap(), 16.0);
1491 }
1492
1493 #[test]
1494 fn test_parse_cpu_millicore() {
1495 assert_eq!(parse_cpu("500m").unwrap(), 0.5);
1496 assert_eq!(parse_cpu("250m").unwrap(), 0.25);
1497 assert_eq!(parse_cpu("1000m").unwrap(), 1.0);
1498 assert_eq!(parse_cpu("100m").unwrap(), 0.1);
1499 }
1500
1501 #[test]
1502 fn test_parse_cpu_invalid() {
1503 assert!(parse_cpu("").is_err());
1504 assert!(parse_cpu("abc").is_err());
1505 assert!(parse_cpu("m").is_err());
1506 }
1507
1508 #[test]
1509 fn test_parse_memory_binary_suffixes() {
1510 assert_eq!(parse_memory_bytes("1Ki").unwrap(), 1024);
1511 assert_eq!(parse_memory_bytes("1Mi").unwrap(), 1024 * 1024);
1512 assert_eq!(parse_memory_bytes("1Gi").unwrap(), 1024 * 1024 * 1024);
1513 assert_eq!(parse_memory_bytes("4Gi").unwrap(), 4 * 1024 * 1024 * 1024);
1514 assert_eq!(parse_memory_bytes("512Mi").unwrap(), 512 * 1024 * 1024);
1515 assert_eq!(
1516 parse_memory_bytes("1Ti").unwrap(),
1517 1024u64 * 1024 * 1024 * 1024
1518 );
1519 }
1520
1521 #[test]
1522 fn test_parse_memory_decimal_suffixes() {
1523 assert_eq!(parse_memory_bytes("1k").unwrap(), 1000);
1524 assert_eq!(parse_memory_bytes("1M").unwrap(), 1_000_000);
1525 assert_eq!(parse_memory_bytes("1G").unwrap(), 1_000_000_000);
1526 assert_eq!(parse_memory_bytes("1T").unwrap(), 1_000_000_000_000);
1527 }
1528
1529 #[test]
1530 fn test_parse_memory_plain_bytes() {
1531 assert_eq!(parse_memory_bytes("1024").unwrap(), 1024);
1532 assert_eq!(parse_memory_bytes("0").unwrap(), 0);
1533 }
1534
1535 #[test]
1536 fn test_parse_memory_invalid() {
1537 assert!(parse_memory_bytes("").is_err());
1538 assert!(parse_memory_bytes("abc").is_err());
1539 assert!(parse_memory_bytes("Gi").is_err());
1540 }
1541
1542 #[test]
1543 fn test_parse_memory_fractional() {
1544 assert_eq!(parse_memory_bytes("0.5Gi").unwrap(), GI / 2);
1545 assert_eq!(parse_memory_bytes("1.5Gi").unwrap(), GI + GI / 2);
1546 }
1547
1548 #[test]
1551 fn test_catalog_has_entries_for_all_cloud_platforms() {
1552 assert!(!catalog_for_platform(Platform::Aws).is_empty());
1553 assert!(!catalog_for_platform(Platform::Gcp).is_empty());
1554 assert!(!catalog_for_platform(Platform::Azure).is_empty());
1555 }
1556
1557 #[test]
1558 fn test_catalog_no_entries_for_non_cloud_platforms() {
1559 assert!(catalog_for_platform(Platform::Local).is_empty());
1560 assert!(catalog_for_platform(Platform::Kubernetes).is_empty());
1561 }
1562
1563 #[test]
1564 fn test_find_known_instance_type() {
1565 let spec =
1566 find_instance_type(Platform::Aws, "m7g.2xlarge").expect("should find m7g.2xlarge");
1567 assert_eq!(spec.vcpu, 8);
1568 assert_eq!(spec.memory_bytes, 32 * GI);
1569 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1570 }
1571
1572 #[test]
1573 fn test_find_aws_c8i_nested_virt_instance_type() {
1574 let spec = find_instance_type(Platform::Aws, "c8i.large").expect("should find c8i.large");
1575 assert_eq!(spec.vcpu, 2);
1576 assert_eq!(spec.memory_bytes, 4 * GI);
1577 assert_eq!(spec.family, InstanceFamily::ComputeOptimized);
1578 assert_eq!(spec.architecture, Architecture::X86_64);
1579 assert!(spec.is_nested_virt_capable());
1580 }
1581
1582 #[test]
1583 fn test_find_unknown_instance_type() {
1584 assert!(find_instance_type(Platform::Aws, "nonexistent.xlarge").is_none());
1585 }
1586
1587 #[test]
1588 fn test_find_wrong_platform() {
1589 assert!(find_instance_type(Platform::Gcp, "m7g.2xlarge").is_none());
1590 }
1591
1592 #[test]
1593 fn test_to_machine_profile() {
1594 let spec = find_instance_type(Platform::Aws, "m7g.2xlarge").unwrap();
1595 let profile = spec.to_machine_profile();
1596 assert_eq!(profile.cpu, "8.0");
1597 assert_eq!(profile.memory_bytes, 32 * GI);
1598 assert_eq!(profile.ephemeral_storage_bytes, 20 * GI);
1599 assert!(profile.gpu.is_none());
1600 }
1601
1602 #[test]
1603 fn test_to_machine_profile_with_gpu() {
1604 let spec = find_instance_type(Platform::Aws, "p4d.24xlarge").unwrap();
1605 let profile = spec.to_machine_profile();
1606 let gpu = profile.gpu.as_ref().expect("should have GPU");
1607 assert_eq!(gpu.gpu_type, "nvidia-a100");
1608 assert_eq!(gpu.count, 8);
1609 }
1610
1611 #[test]
1614 fn test_select_burstable_for_small_workload() {
1615 let req = WorkloadRequirements {
1616 total_cpu_at_desired: 1.0,
1617 total_memory_bytes_at_desired: 2 * GI,
1618 total_cpu_at_max: 1.0,
1619 total_memory_bytes_at_max: 2 * GI,
1620 max_cpu_per_container: 0.5,
1621 max_memory_per_container: 1 * GI,
1622 max_ephemeral_storage_bytes: 10 * GI,
1623 gpu: None,
1624 architecture: None,
1625 nested_virt: false,
1626 };
1627 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1628 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1629 assert_eq!(spec.family, InstanceFamily::Burstable);
1630 }
1631
1632 #[test]
1633 fn test_selects_smallest_burstable_machine_with_real_headroom() {
1634 let req = WorkloadRequirements {
1635 total_cpu_at_desired: 1.0,
1636 total_memory_bytes_at_desired: 2 * GI,
1637 total_cpu_at_max: 1.0,
1638 total_memory_bytes_at_max: 2 * GI,
1639 max_cpu_per_container: 1.0,
1640 max_memory_per_container: 2 * GI,
1641 max_ephemeral_storage_bytes: 10 * GI,
1642 gpu: None,
1643 architecture: None,
1644 nested_virt: false,
1645 };
1646
1647 let selection = select_instance_type(Platform::Aws, &req).unwrap();
1648
1649 assert_eq!(selection.instance_type, "t4g.medium");
1650 assert_eq!(selection.min_machines, 1);
1651 assert_eq!(selection.max_machines, 1);
1652 }
1653
1654 #[test]
1655 fn test_select_general_purpose_for_standard_workload() {
1656 let req = WorkloadRequirements {
1658 total_cpu_at_desired: 20.0,
1659 total_memory_bytes_at_desired: 80 * GI,
1660 total_cpu_at_max: 20.0,
1661 total_memory_bytes_at_max: 80 * GI,
1662 max_cpu_per_container: 2.0,
1663 max_memory_per_container: 8 * GI,
1664 max_ephemeral_storage_bytes: 10 * GI,
1665 gpu: None,
1666 architecture: None,
1667 nested_virt: false,
1668 };
1669 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1670 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1671 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1672 }
1673
1674 #[test]
1675 fn test_select_general_purpose_even_for_cpu_heavy() {
1676 let req = WorkloadRequirements {
1678 total_cpu_at_desired: 20.0,
1679 total_memory_bytes_at_desired: 20 * GI,
1680 total_cpu_at_max: 20.0,
1681 total_memory_bytes_at_max: 20 * GI,
1682 max_cpu_per_container: 2.0,
1683 max_memory_per_container: 2 * GI,
1684 max_ephemeral_storage_bytes: 10 * GI,
1685 gpu: None,
1686 architecture: None,
1687 nested_virt: false,
1688 };
1689 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1690 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1691 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1692 }
1693
1694 #[test]
1695 fn test_select_storage_optimized_for_large_ephemeral() {
1696 let req = WorkloadRequirements {
1697 total_cpu_at_desired: 8.0,
1698 total_memory_bytes_at_desired: 32 * GI,
1699 total_cpu_at_max: 8.0,
1700 total_memory_bytes_at_max: 32 * GI,
1701 max_cpu_per_container: 2.0,
1702 max_memory_per_container: 8 * GI,
1703 max_ephemeral_storage_bytes: 500 * GI,
1704 gpu: None,
1705 architecture: Some(Architecture::X86_64),
1706 nested_virt: false,
1707 };
1708 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1709 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1710 assert_eq!(spec.family, InstanceFamily::StorageOptimized);
1711 }
1712
1713 #[test]
1714 fn test_select_gpu_instance() {
1715 let req = WorkloadRequirements {
1716 total_cpu_at_desired: 8.0,
1717 total_memory_bytes_at_desired: 32 * GI,
1718 total_cpu_at_max: 8.0,
1719 total_memory_bytes_at_max: 32 * GI,
1720 max_cpu_per_container: 4.0,
1721 max_memory_per_container: 16 * GI,
1722 max_ephemeral_storage_bytes: 10 * GI,
1723 gpu: Some(GpuSpec {
1724 gpu_type: "nvidia-a100".to_string(),
1725 count: 1,
1726 }),
1727 architecture: Some(Architecture::X86_64),
1728 nested_virt: false,
1729 };
1730 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1731 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1732 assert_eq!(spec.family, InstanceFamily::GpuCompute);
1733 assert!(spec.gpu.is_some());
1734 }
1735
1736 #[test]
1737 fn test_select_uses_each_cloud_image_target_architecture() {
1738 let req = WorkloadRequirements {
1739 total_cpu_at_desired: 4.0,
1740 total_memory_bytes_at_desired: 16 * GI,
1741 total_cpu_at_max: 4.0,
1742 total_memory_bytes_at_max: 16 * GI,
1743 max_cpu_per_container: 1.0,
1744 max_memory_per_container: 4 * GI,
1745 max_ephemeral_storage_bytes: 10 * GI,
1746 gpu: None,
1747 architecture: None,
1748 nested_virt: false,
1749 };
1750 for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
1751 let sel = select_instance_type(platform, &req)
1752 .unwrap_or_else(|error| panic!("selection failed for {platform}: {error}"));
1753 let spec = find_instance_type(platform, sel.instance_type)
1754 .expect("selected machine should exist in the catalog");
1755 assert_eq!(
1756 Some(spec.architecture),
1757 default_architecture(platform),
1758 "machine architecture must match the image target for {platform}"
1759 );
1760 }
1761 }
1762
1763 #[test]
1764 fn test_machine_count_reasonable() {
1765 let req = WorkloadRequirements {
1767 total_cpu_at_desired: 20.0,
1768 total_memory_bytes_at_desired: 40 * GI,
1769 total_cpu_at_max: 20.0,
1770 total_memory_bytes_at_max: 40 * GI,
1771 max_cpu_per_container: 1.0,
1772 max_memory_per_container: 2 * GI,
1773 max_ephemeral_storage_bytes: 10 * GI,
1774 gpu: None,
1775 architecture: None,
1776 nested_virt: false,
1777 };
1778 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1779 assert!(sel.min_machines >= 1);
1780 assert!(sel.max_machines <= MAX_MACHINES_PER_CLUSTER);
1781 assert!(sel.max_machines >= sel.min_machines);
1782 }
1783
1784 #[test]
1785 fn test_instance_size_capped_at_8_vcpu() {
1786 let req = WorkloadRequirements {
1788 total_cpu_at_desired: 70.0,
1789 total_memory_bytes_at_desired: 140 * GI,
1790 total_cpu_at_max: 70.0,
1791 total_memory_bytes_at_max: 140 * GI,
1792 max_cpu_per_container: 2.0,
1793 max_memory_per_container: 4 * GI,
1794 max_ephemeral_storage_bytes: 10 * GI,
1795 gpu: None,
1796 architecture: None,
1797 nested_virt: false,
1798 };
1799 let sel = select_instance_type(Platform::Gcp, &req).unwrap();
1800 let spec = find_instance_type(Platform::Gcp, sel.instance_type).unwrap();
1801 assert!(
1802 spec.vcpu <= MAX_STANDARD_VCPU,
1803 "selected {} with {} vCPUs, expected <= {}",
1804 spec.name,
1805 spec.vcpu,
1806 MAX_STANDARD_VCPU
1807 );
1808 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1809 assert!(sel.max_machines > 1);
1811 }
1812
1813 #[test]
1814 fn test_larger_autoscaled_workload_gets_reasonable_instance() {
1815 let req = WorkloadRequirements {
1818 total_cpu_at_desired: 70.0,
1819 total_memory_bytes_at_desired: 140 * GI,
1820 total_cpu_at_max: 70.0, total_memory_bytes_at_max: 140 * GI, max_cpu_per_container: 2.0,
1823 max_memory_per_container: 4 * GI,
1824 max_ephemeral_storage_bytes: 20 * GI,
1825 gpu: None,
1826 architecture: None,
1827 nested_virt: false,
1828 };
1829 let sel = select_instance_type(Platform::Gcp, &req).unwrap();
1830 assert_eq!(sel.instance_type, "n2-standard-8");
1832 assert!(sel.max_machines >= 2);
1833 }
1834
1835 #[test]
1842 fn test_select_aws_picks_m8i_when_nested_virt_required() {
1843 let req = WorkloadRequirements {
1844 total_cpu_at_desired: 4.0,
1845 total_memory_bytes_at_desired: 8 * GI,
1846 total_cpu_at_max: 4.0,
1847 total_memory_bytes_at_max: 8 * GI,
1848 max_cpu_per_container: 4.0,
1849 max_memory_per_container: 8 * GI,
1850 max_ephemeral_storage_bytes: 10 * GI,
1851 gpu: None,
1852 architecture: Some(Architecture::X86_64),
1853 nested_virt: true,
1854 };
1855 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1856 assert!(
1857 sel.instance_type.starts_with("m8i.")
1858 || sel.instance_type.starts_with("c8i.")
1859 || sel.instance_type.starts_with("r8i."),
1860 "expected an m8i/c8i/r8i instance, got {}",
1861 sel.instance_type
1862 );
1863 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1864 assert!(spec.is_nested_virt_capable());
1865 }
1866
1867 #[test]
1868 fn test_select_aws_defaults_to_image_target_architecture() {
1869 let req = WorkloadRequirements {
1870 total_cpu_at_desired: 4.0,
1871 total_memory_bytes_at_desired: 8 * GI,
1872 total_cpu_at_max: 4.0,
1873 total_memory_bytes_at_max: 8 * GI,
1874 max_cpu_per_container: 4.0,
1875 max_memory_per_container: 8 * GI,
1876 max_ephemeral_storage_bytes: 10 * GI,
1877 gpu: None,
1878 architecture: None,
1879 nested_virt: false,
1880 };
1881 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1882 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1883 assert_eq!(spec.architecture, Architecture::Arm64);
1884 }
1885
1886 #[test]
1887 fn test_cloud_defaults_match_image_target_architectures() {
1888 for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
1889 let target = BinaryTarget::defaults_for_platform(platform)
1890 .into_iter()
1891 .next()
1892 .expect("managed cloud should have a default image target");
1893 let image_architecture = match target.oci_arch() {
1894 "arm64" => Architecture::Arm64,
1895 "amd64" => Architecture::X86_64,
1896 architecture => {
1897 panic!("unsupported managed-cloud image architecture {architecture}")
1898 }
1899 };
1900
1901 assert_eq!(default_architecture(platform), Some(image_architecture));
1902 }
1903 }
1904
1905 #[test]
1907 fn test_select_aws_uses_graviton_for_explicit_arm64() {
1908 let req = WorkloadRequirements {
1909 total_cpu_at_desired: 4.0,
1910 total_memory_bytes_at_desired: 8 * GI,
1911 total_cpu_at_max: 4.0,
1912 total_memory_bytes_at_max: 8 * GI,
1913 max_cpu_per_container: 4.0,
1914 max_memory_per_container: 8 * GI,
1915 max_ephemeral_storage_bytes: 10 * GI,
1916 gpu: None,
1917 architecture: Some(Architecture::Arm64),
1918 nested_virt: false,
1919 };
1920 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1921 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1922 assert_eq!(spec.architecture, Architecture::Arm64);
1923 }
1924
1925 #[test]
1926 fn test_select_rejects_explicit_architecture_missing_from_cloud_catalog() {
1927 let req = WorkloadRequirements {
1928 total_cpu_at_desired: 1.0,
1929 total_memory_bytes_at_desired: 2 * GI,
1930 total_cpu_at_max: 1.0,
1931 total_memory_bytes_at_max: 2 * GI,
1932 max_cpu_per_container: 1.0,
1933 max_memory_per_container: 2 * GI,
1934 max_ephemeral_storage_bytes: 10 * GI,
1935 gpu: None,
1936 architecture: Some(Architecture::Arm64),
1937 nested_virt: false,
1938 };
1939
1940 let error = select_instance_type(Platform::Gcp, &req)
1941 .expect_err("GCP catalog has no ARM64 machine");
1942
1943 assert!(error.contains("architecture Arm64 is unavailable"));
1944 }
1945
1946 #[test]
1947 fn test_profile_has_required_fields() {
1948 let req = WorkloadRequirements {
1949 total_cpu_at_desired: 4.0,
1950 total_memory_bytes_at_desired: 16 * GI,
1951 total_cpu_at_max: 4.0,
1952 total_memory_bytes_at_max: 16 * GI,
1953 max_cpu_per_container: 1.0,
1954 max_memory_per_container: 4 * GI,
1955 max_ephemeral_storage_bytes: 10 * GI,
1956 gpu: None,
1957 architecture: None,
1958 nested_virt: false,
1959 };
1960 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1961 assert!(!sel.profile.cpu.is_empty());
1962 assert!(sel.profile.memory_bytes > 0);
1963 assert!(sel.profile.ephemeral_storage_bytes > 0);
1964 }
1965
1966 #[test]
1967 fn test_error_for_unsupported_gpu_type() {
1968 let req = WorkloadRequirements {
1969 total_cpu_at_desired: 8.0,
1970 total_memory_bytes_at_desired: 32 * GI,
1971 total_cpu_at_max: 8.0,
1972 total_memory_bytes_at_max: 32 * GI,
1973 max_cpu_per_container: 4.0,
1974 max_memory_per_container: 16 * GI,
1975 max_ephemeral_storage_bytes: 10 * GI,
1976 gpu: Some(GpuSpec {
1977 gpu_type: "amd-mi300".to_string(),
1978 count: 1,
1979 }),
1980 architecture: None,
1981 nested_virt: false,
1982 };
1983 let result = select_instance_type(Platform::Aws, &req);
1984 assert!(result.is_err());
1985 }
1986
1987 #[test]
1988 fn test_catalog_instance_types_sorted_by_vcpu_within_family() {
1989 for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
1992 let entries = catalog_for_platform(platform);
1993 let mut by_family: std::collections::HashMap<_, Vec<_>> =
1994 std::collections::HashMap::new();
1995 for entry in entries {
1996 by_family
1997 .entry(format!("{:?}", entry.family))
1998 .or_default()
1999 .push(entry);
2000 }
2001 for (family, instances) in &by_family {
2002 for window in instances.windows(2) {
2003 assert!(
2004 window[0].vcpu <= window[1].vcpu,
2005 "catalog not sorted by vcpu for {platform}/{family}: {} ({}) > {} ({})",
2006 window[0].name,
2007 window[0].vcpu,
2008 window[1].name,
2009 window[1].vcpu
2010 );
2011 }
2012 }
2013 }
2014 }
2015}