Coverage for simulator/sim_types.py: 90%
467 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 04:47 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 04:47 +0000
1from __future__ import annotations
3import pandas as pd
4import numpy as np
6from typing import Optional
7from typing import ClassVar
9from abc import ABC
10from abc import abstractmethod
12from dataclasses import dataclass
13from dataclasses import field
15from enum import Enum
18class GPUType(Enum):
19 A100 = "A100"
20 H100 = "H100"
21 H200 = "H200"
22 GB200 = "GB200"
24 def __lt__(self, other: object) -> bool:
25 if not isinstance(other, GPUType):
26 return NotImplemented
27 order = [GPUType.A100, GPUType.H100, GPUType.H200, GPUType.GB200]
28 return order.index(self) < order.index(other)
31class QualityLevel(Enum):
32 ORIGINAL = "original"
33 HIGH = "high"
34 MEDIUM = "medium"
35 LOW = "low"
38# Pixel counts per quality level (16:10 aspect ratio).
39# Latency data is profiled at MEDIUM resolution.
40RESOLUTION_PIXELS: dict[QualityLevel, int] = {
41 QualityLevel.HIGH: 1280 * 800,
42 QualityLevel.MEDIUM: 640 * 400,
43 QualityLevel.LOW: 320 * 200,
44}
47class Model(Enum):
48 GEMMA = "gemma"
49 FLUX = "flux"
50 HF = "hf" # HunyuanFramePack
51 HF_VAE = "hf_vae" # HunyuanFramePack VAE
52 FT = "ft" # FantasyTalking
53 FT_VAE = "ft_vae" # FantasyTalking VAE
54 UPSCALER = "upscaler"
55 OTHERS = "others" # YOLO + Kokoro
58# Used for FIFO
59MODEL_ORDER: dict[Model, int] = {
60 Model.GEMMA: 0,
61 Model.FLUX: 1,
62 Model.OTHERS: 2,
63 Model.HF: 3,
64 Model.HF_VAE: 4,
65 Model.FT: 5,
66 Model.FT_VAE: 6,
67 Model.UPSCALER: 7,
68}
71@dataclass
72class ModelAllocation(ABC):
73 model: ClassVar[Model]
75 # policy TODO
76 # workflow TODO
77 gpu_type: GPUType
78 devices: int = 1
79 replicas: int = 0 # No replicas by default
80 work: int = 0
81 time: float = 0.0
82 time_first: float = 0.0
83 energy: float = 0.0
84 cost: float = 0.0
86 def __str__(self) -> str:
87 if self.replicas <= 0:
88 assert self.time == 0.0, f"time must be 0 when no replicas, got {self.time:.2f}"
89 assert self.energy == 0.0, f"energy must be 0 when no replicas, got {self.energy:.2f}"
90 return "--"
91 return \
92 f"devices={self.devices:2d}, " \
93 f"replicas={self.replicas}, " \
94 f"work={self.work}, " \
95 f"time={self.time:.2f} secs, " \
96 f"time_first={self.time_first:.2f} secs, " \
97 f"energy={self.energy / 60.0 / 60.0:.2f} Wh, " \
98 f"cost=${self.cost:.2f}"
100 def __repr__(self) -> str:
101 return self.__str__()
103 def __post_init__(self) -> None:
104 if self.replicas > 0:
105 return
106 if self.time != 0.0 or self.energy != 0.0:
107 raise ValueError(
108 f"time and energy must be 0.0 when no replicas, got time={self.time:.2f}, energy={self.energy:.2f}")
110 def get_num_gpus(self) -> int:
111 if self.replicas <= 0:
112 return 0
113 return self.devices * self.replicas
115 def disable(self) -> None:
116 self.devices = 0
117 self.replicas = 0
118 self.time = 0.0
119 self.time_first = 0.0
120 self.energy = 0.0
122 @abstractmethod
123 def calculate_time(
124 self,
125 policy: Policy,
126 workflow: WorkflowConfig,
127 latency_data: LatencyData,
128 work_pct: float = 1.0,
129 ) -> float:
130 ...
132 @abstractmethod
133 def calculate_time_first(
134 self,
135 policy: Policy,
136 workflow: WorkflowConfig,
137 latency_data: LatencyData,
138 ) -> float:
139 ...
141 @abstractmethod
142 def calculate_energy(
143 self,
144 workflow: WorkflowConfig,
145 power_data: Optional[PowerData] = None,
146 total_time_s: float = 0.0,
147 ) -> float:
148 ...
150 def calculate_cost(
151 self,
152 policy: Policy,
153 total_time_s: float = 0.0,
154 ) -> float:
155 """Calculate the cost for this model allocation."""
156 SECONDS_IN_HOUR = 60 * 60
157 gpu_cost = policy.gpu_cost[self.gpu_type]
158 self.cost = total_time_s * (self.get_num_gpus() * gpu_cost) / SECONDS_IN_HOUR
159 return self.cost
161 def calculate(
162 self,
163 policy: Policy,
164 workflow: WorkflowConfig,
165 latency_data: LatencyData,
166 power_data: Optional[PowerData] = None,
167 total_time_s: float = 0.0,
168 work_pct: float = 1.0,
169 ) -> None:
170 """Calculate all the values for this model allocation."""
171 self.calculate_time(policy, workflow, latency_data, work_pct)
172 self.calculate_time_first(policy, workflow, latency_data)
173 self.calculate_cost(policy, total_time_s)
174 self.calculate_energy(workflow, power_data, total_time_s)
176 def get_max_replicas(
177 self,
178 workflow: WorkflowConfig,
179 ) -> int:
180 """Get the maximum number of replicas that can leverage parallelism."""
181 return 1
184class Objective(Enum):
185 FIFO = "fifo"
186 TIME = "time"
187 TTFF = "ttff"
188 COST = "cost"
189 ENERGY = "energy"
190 TIME_COST = "time_cost"
191 TTFF_COST = "ttff_cost"
192 ENERGY_COST = "energy_cost"
193 TIME_ENERGY = "time_energy"
194 RANDOM = "random"
195 NONE = "none"
197 TTFF_THEN_TIME = "ttff_then_time" # first minimize ttff, then minimize time
199 def is_monotonic(self) -> bool:
200 return self not in {Objective.RANDOM, Objective.FIFO}
203@dataclass
204class WorkflowConfig:
205 total_video_seconds: int
206 total_scenes: int
207 total_frames: dict[Model, int]
208 total_subscenes: int
209 per_subscene_frames: dict[Model, int]
210 # default per-frame number of denoising steps
211 num_steps: dict[Model, int]
212 # supported number of generation frames
213 hf_frames: list[int]
214 ft_frames: list[int]
215 frames_per_step_idx: int
216 # target output resolution (default: HIGH)
217 target_resolution: QualityLevel = QualityLevel.HIGH
219 # total input tokens
220 total_input_tokens: int = 0
222 # work per model (determines parallelism; work > 1 means parallelizable across replicas)
223 # models included in the workflow are derived from the keys of this dict
224 model_work: dict[Model, int] = field(default_factory=dict)
226 @property
227 def models(self) -> list[Model]:
228 """Models included in the workflow (derived from model_work keys)."""
229 return list(self.model_work.keys())
231 @property
232 def work(self) -> dict[Model, int]:
233 """Units of work per model (0 for models not in the workflow)."""
234 return {
235 model_name: self.model_work.get(model_name, 0)
236 for model_name in Model
237 }
239 def get_model_order(self) -> list[Model]:
240 """Get ordered list of models in the workflow, sorted by MODEL_ORDER."""
241 return sorted(
242 [m for m in self.models if m in MODEL_ORDER],
243 key=lambda m: MODEL_ORDER[m],
244 )
246 def get_resolution_scale(self, use_upscaler: bool) -> float:
247 """Compute latency scaling factor based on target resolution.
249 Latency data is profiled at MEDIUM resolution. The scale factor
250 adjusts for the actual generation resolution:
252 1. Upscaler used, HIGH → 1.0 (models generate at MEDIUM)
253 2. Upscaler used, MEDIUM → LOW / MEDIUM (models generate at LOW)
254 3. No upscaler, HIGH → HIGH / MEDIUM (scale up)
255 4. No upscaler, MEDIUM → 1.0
256 5. No upscaler, LOW → LOW / MEDIUM (scale down)
257 """
258 if use_upscaler:
259 assert self.target_resolution in (QualityLevel.HIGH, QualityLevel.MEDIUM), \
260 "Upscaler can only be used when target resolution is HIGH or MEDIUM"
261 if self.target_resolution == QualityLevel.HIGH:
262 return 1.0
263 # MEDIUM target with upscaler: generate at LOW, upscale to MEDIUM
264 return RESOLUTION_PIXELS[QualityLevel.LOW] / RESOLUTION_PIXELS[QualityLevel.MEDIUM]
265 if self.target_resolution == QualityLevel.MEDIUM:
266 return 1.0
267 return RESOLUTION_PIXELS[self.target_resolution] / RESOLUTION_PIXELS[QualityLevel.MEDIUM]
269 def is_parallelizable(self, model: Model) -> bool:
270 """Whether the given model can be parallelized across multiple replicas."""
271 return self.model_work.get(model, 0) > 1
273 def filter_parallelizable_models(
274 self,
275 models: list[Model],
276 disaggregation: dict[Model, bool],
277 ) -> list[Model]:
278 filtered_models = [
279 model
280 for model in models
281 if self.is_parallelizable(model)
282 ]
283 # Remove VAE models when their parent model disaggregation is disabled
284 if not disaggregation.get(Model.HF, False):
285 filtered_models = [m for m in filtered_models if m != Model.HF_VAE]
286 if not disaggregation.get(Model.FT, False):
287 filtered_models = [m for m in filtered_models if m != Model.FT_VAE]
288 return filtered_models
290 def __post_init__(self) -> None:
291 assert self.total_frames[Model.HF] > self.per_subscene_frames[Model.HF]
292 assert self.total_frames[Model.FT] > self.per_subscene_frames[Model.FT]
294 # If no models specified, populate defaults for all models
295 if not self.model_work:
296 defaults: dict[Model, int] = {
297 Model.GEMMA: 1,
298 Model.FLUX: 1,
299 Model.HF: self.total_subscenes,
300 Model.HF_VAE: self.total_frames[Model.HF],
301 Model.FT: self.total_subscenes,
302 Model.FT_VAE: self.total_frames[Model.FT],
303 Model.UPSCALER: self.total_frames[Model.FT],
304 Model.OTHERS: 1,
305 }
306 for model, work in defaults.items():
307 self.model_work[model] = work
308 if self.target_resolution != QualityLevel.HIGH:
309 if Model.UPSCALER in self.model_work:
310 del self.model_work[Model.UPSCALER]
312 @property
313 def num_frames(self) -> int:
314 """Number of frames generated by the workflow."""
315 if Model.FT in self.total_frames:
316 return self.total_frames[Model.FT]
317 return 0
320class ActionName(Enum):
321 MERGE = "merge"
322 ADD_DEVICE = "add device"
323 ADD_REPLICA = "add replica"
324 ADD_DEVICE_REPLICA = "add device replica"
325 ADD_INSTANCE = "add instance"
326 REMOVE_DEVICE = "remove device"
327 REMOVE_REPLICA = "remove replica"
330@dataclass
331class Action:
332 """
333 Optimization action to take.
334 """
335 name: ActionName
336 model: Model
337 gpu_type: GPUType
338 models: dict[GPUType, dict[Model, list[ModelAllocation]]]
340 action_result: Result = field(repr=False)
342 arrival_time_s: float = 0.0 # For FIFO scheduling
344 # Derived fields from action_result (not passed by caller)
345 time: float = field(init=False) # Total execution time
346 ttff: float = field(init=False) # Time to first frame
347 cost: float = field(init=False) # Cost in $
348 energy: float = field(init=False) # Energy in W*s
350 def __post_init__(self) -> None:
351 # ---- type checks ----
352 if not isinstance(self.model, Model):
353 raise ValueError(f"Model {self.model} [{type(self.model)}] not supported")
354 if not isinstance(self.name, ActionName):
355 raise ValueError(f"Action name {self.name} [{type(self.name)}] not supported")
356 if not isinstance(self.models, dict):
357 raise ValueError(f"models must be a dict, got {type(self.models)}")
358 if not isinstance(self.gpu_type, GPUType):
359 raise ValueError(f"Device type {self.gpu_type} [{type(self.gpu_type)}] not supported")
360 """
361 if not isinstance(self.allocation_id, int) or self.allocation_id < 0:
362 raise ValueError(f"Allocation ID {self.allocation_id} must be a non-negative integer")
363 if self.num_replicas <= 0:
364 raise ValueError(f"num_replicas {self.num_replicas} must be > 0")
365 if self.num_devices <= 0:
366 raise ValueError(f"num_devices {self.num_devices} must be > 0")
367 """
368 # ---- derive values ----
369 self.time = self.action_result.total_time_s
370 self.ttff = self.action_result.ttff_s
371 self.cost = self.action_result.cost
372 self.energy = self.action_result.total_energy
373 if self.cost < 0.0:
374 raise ValueError("cost must be >= 0")
376 def __str__(self) -> str:
377 return (
378 f"Action("
379 f"{self.name.value}, "
380 f"model={self.model.value}, "
381 f"gpu={self.gpu_type.value}, "
382 f"time={self.time:.2f} s, "
383 f"ttff={self.ttff:.2f} s, "
384 f"cost=${self.cost:.2f}, "
385 f"time*cost={self.time_cost():.2f}, "
386 f"ttff*cost={self.ttff_cost():.2f}, "
387 f"energy*cost={self.energy_cost():.2f}, "
388 f"time*energy={self.time_energy():.2f}, "
389 f"energy={self.energy:.2f} Ws, "
390 f"models={self.models}"
391 f")"
392 )
394 def time_cost(self) -> float:
395 """We use improvement in time * $."""
396 if self.time <= 0:
397 return self.cost
398 if self.cost <= 0:
399 return self.time
400 return self.time * self.cost
402 def ttff_cost(self) -> float:
403 """We use improvement in TTFF * $."""
404 if self.ttff <= 0:
405 return self.cost
406 if self.cost <= 0:
407 return self.ttff
408 return self.ttff * self.cost
410 def energy_cost(self) -> float:
411 """We use improvement in Wh * $."""
412 if self.cost <= 0:
413 return self.energy
414 if self.energy <= 0:
415 return self.cost
416 return self.energy * self.cost
418 def time_energy(self) -> float:
419 """We use improvement in TTFF * Wh."""
420 if self.energy <= 0:
421 return self.time
422 if self.time <= 0:
423 return self.energy
424 return self.time * self.energy
426 def get_order(self) -> int:
427 " ""For FIFO scheduling."" "
428 return MODEL_ORDER[self.model]
430 def get_metric(
431 self,
432 obj: Objective,
433 switch_objective: bool = False,
434 ) -> float:
435 if obj == Objective.RANDOM:
436 return 0.0
437 if obj == Objective.TIME:
438 return self.time
439 if obj == Objective.TTFF:
440 return self.ttff
441 if obj == Objective.COST:
442 return self.cost
443 if obj == Objective.ENERGY:
444 return self.energy
445 if obj == Objective.TIME_COST:
446 return self.time_cost()
447 if obj == Objective.TTFF_COST:
448 return self.ttff_cost()
449 if obj == Objective.ENERGY_COST:
450 return self.energy_cost()
451 if obj == Objective.TIME_ENERGY:
452 return self.time_energy()
453 if obj == Objective.FIFO:
454 # return self.get_order()
455 return 0 # TODO
456 if obj == Objective.TTFF_THEN_TIME:
457 if switch_objective:
458 return self.time
459 else:
460 return self.ttff
461 raise ValueError(f"Unknown objective {obj}")
464@dataclass
465class Result:
466 total_time_s: float = 0.0
467 first_chunk_time: float = 0.0 # Time to first chunk
468 ttff_s: float = 0.0 # Time to first frame (accounts for total time and workflow length)
469 tbf_s: float = 0.0 # Time between frames
470 total_energy: float = 0.0 # Watts x second
471 cost: float = 0.0 # Total $ cost
472 gpus_used: dict[GPUType, int] = field(default_factory=dict)
473 gpus_total: dict[GPUType, int] = field(default_factory=dict)
474 models: dict[GPUType, dict[Model, list[ModelAllocation]]] = field(default_factory=dict)
476 def __post_init__(self) -> None:
477 assert self.total_time_s >= 0.0, f"total_time_s={self.total_time_s} must be >= 0.0"
478 assert self.first_chunk_time >= 0.0, f"first_chunk_time={self.first_chunk_time} must be >= 0.0"
479 assert self.ttff_s >= 0.0, f"ttff_s={self.ttff_s} must be >= 0.0"
480 assert self.tbf_s >= 0.0, f"tbf_s={self.tbf_s} must be >= 0.0"
481 assert self.total_energy >= 0.0, f"total_energy={self.total_energy} must be >= 0.0"
482 assert self.cost >= 0.0, f"cost={self.cost} must be >= 0.0"
483 assert len(self.gpus_used) >= 0, f"gpus_used cannot be empty: {self.gpus_used}"
484 for gpu_used in self.gpus_used.values():
485 assert gpu_used >= 0, f"all gpus_used value {self.gpus_used} must be >= 0"
487 def to_csv(self) -> str:
488 num_a100 = self.gpus_used.get(GPUType.A100, 0)
489 num_h100 = self.gpus_used.get(GPUType.H100, 0)
490 num_h200 = self.gpus_used.get(GPUType.H200, 0)
491 num_gb200 = self.gpus_used.get(GPUType.GB200, 0)
492 return (
493 f"{num_a100},{num_h100},{num_h200},{num_gb200},"
494 f"{self.ttff_s:.2f},{self.tbf_s:.2f},{self.cost:.2f},"
495 f"{self.total_time_s:.2f},{self.total_energy:.2f}"
496 )
498 def __str__(self) -> str:
499 SECONDS_IN_HOUR = 60 * 60
500 return (
501 f"Time:{self.total_time_s:.2f} s TTFF:{self.ttff_s:.2f} s "
502 f"Cost:${self.cost:.2f} TTFF*Cost:{self.ttff_s * self.cost:.2f} "
503 f"Energy:{self.total_energy / SECONDS_IN_HOUR / 1000:.2f} kWh "
504 f"GPUS: {num_gpus_to_str(self.gpus_used)}"
505 )
507 def __repr__(self) -> str:
508 return self.__str__()
511@dataclass
512class LatencyGPUTypeData:
513 gpu_type: GPUType
514 # TP -> latency mappings
515 flux: dict[int, float] = field(default_factory=dict)
516 hf: dict[int, float] = field(default_factory=dict)
517 hf_high: dict[int, float] = field(default_factory=dict)
518 hf_vae: dict[int, float] = field(default_factory=dict)
519 hf_vae_high: dict[int, float] = field(default_factory=dict)
520 ft: dict[int, float] = field(default_factory=dict)
521 ft_high: dict[int, float] = field(default_factory=dict)
522 ft_vae: dict[int, float] = field(default_factory=dict)
523 ft_vae_high: dict[int, float] = field(default_factory=dict)
524 upscaler: dict[int, float] = field(default_factory=dict)
525 gemma_first_scene: dict[int, float] = field(default_factory=dict)
526 gemma_per_scene: dict[int, float] = field(default_factory=dict)
527 others: dict[int, float] = field(default_factory=dict)
529 def __getitem__(
530 self,
531 key: Model | tuple[Model, int]
532 ) -> float:
533 if isinstance(key, tuple):
534 assert isinstance(key[0], Model)
535 assert isinstance(key[1], int)
536 model, num_devices = key
537 if model == Model.FLUX:
538 return self.flux[num_devices]
539 if model == Model.HF:
540 return self.hf[num_devices]
541 if model == Model.HF_VAE:
542 return self.hf_vae[num_devices]
543 if model == Model.FT:
544 return self.ft[num_devices]
545 if model == Model.FT_VAE:
546 return self.ft_vae[num_devices]
547 if model == Model.GEMMA:
548 return self.gemma_first_scene[num_devices]
549 if model == Model.UPSCALER:
550 return self.upscaler[num_devices]
551 if model == Model.OTHERS:
552 return self.others[num_devices]
553 raise KeyError(f"Latency for model {key} not found")
555 def __contains__(self, key: Model | tuple[Model, int]) -> bool:
556 if isinstance(key, tuple):
557 assert isinstance(key[0], Model)
558 assert isinstance(key[1], int)
559 model, num_devices = key
560 if model == Model.GEMMA:
561 return num_devices in self.gemma_first_scene
562 if model == Model.FLUX:
563 return num_devices in self.flux
564 if model == Model.HF:
565 return num_devices in self.hf
566 if model == Model.HF_VAE:
567 return num_devices in self.hf_vae
568 if model == Model.FT:
569 return num_devices in self.ft
570 if model == Model.FT_VAE:
571 return num_devices in self.ft_vae
572 if model == Model.UPSCALER:
573 return num_devices in self.upscaler
574 if model == Model.HF_VAE:
575 return num_devices in self.hf_vae
576 if model == Model.OTHERS:
577 return num_devices in self.others
578 return False
580 def get_max_parallelism(self, model: Model) -> int:
581 """Max number of devices supported for the given model."""
582 if model == Model.FLUX:
583 return max(self.flux.keys())
584 if model == Model.HF:
585 return max(self.hf.keys())
586 if model == Model.FT:
587 return max(self.ft.keys())
588 if model == Model.FT_VAE:
589 return max(self.ft_vae.keys())
590 if model == Model.GEMMA:
591 return max(self.gemma_first_scene.keys())
592 if model == Model.UPSCALER:
593 return max(self.upscaler.keys())
594 if model == Model.HF_VAE:
595 return max(self.hf_vae.keys())
596 if model == Model.OTHERS:
597 return max(self.others.keys())
598 raise KeyError(f"Model {model} not found in latency data")
601@dataclass
602class PowerGPUTypeData:
603 gpu_type: GPUType
604 # TP -> power mappings
605 flux: dict[int, float] = field(default_factory=dict)
606 hf: dict[int, float] = field(default_factory=dict)
607 hf_high: dict[int, float] = field(default_factory=dict)
608 hf_vae: dict[int, float] = field(default_factory=dict)
609 hf_vae_high: dict[int, float] = field(default_factory=dict)
610 ft: dict[int, float] = field(default_factory=dict)
611 ft_high: dict[int, float] = field(default_factory=dict)
612 ft_vae: dict[int, float] = field(default_factory=dict)
613 ft_vae_high: dict[int, float] = field(default_factory=dict)
614 upscaler: dict[int, float] = field(default_factory=dict)
615 gemma_first_scene: dict[int, float] = field(default_factory=dict)
616 gemma_per_scene: dict[int, float] = field(default_factory=dict)
617 # Other values
618 idle: float = 0.0 # Idle power in Watts
619 tdp: float = 0.0 # TDP power in Watts
621 def __getitem__(
622 self,
623 key: Model | tuple[Model, int] | str
624 ) -> float:
625 if isinstance(key, tuple):
626 assert isinstance(key[0], Model)
627 assert isinstance(key[1], int)
628 model, devices = key
629 if model == Model.FLUX:
630 return self.flux[devices]
631 if model == Model.HF:
632 return self.hf[devices]
633 if model == Model.HF_VAE:
634 return self.hf_vae[devices]
635 if model == Model.FT:
636 return self.ft[devices]
637 if model == Model.FT_VAE:
638 return self.ft_vae[devices]
639 if model == Model.UPSCALER:
640 return self.upscaler[devices]
641 if isinstance(key, str):
642 if key == "idle":
643 return self.idle
644 if key == "tdp":
645 return self.tdp
646 raise KeyError(f"Power for {key} not found")
649@dataclass
650class LatencyData:
651 gpus: dict[GPUType, LatencyGPUTypeData]
653 def __getitem__(self, gpu_type: GPUType) -> LatencyGPUTypeData:
654 return self.gpus[gpu_type]
656 def __setitem__(
657 self,
658 gpu_type: GPUType,
659 latency_data: LatencyGPUTypeData
660 ) -> None:
661 self.gpus[gpu_type] = latency_data
664@dataclass
665class PowerData:
666 gpus: dict[GPUType, PowerGPUTypeData]
668 def __getitem__(self, gpu_type: GPUType) -> PowerGPUTypeData:
669 return self.gpus[gpu_type]
671 def __setitem__(
672 self,
673 gpu_type: GPUType,
674 power_data: PowerGPUTypeData
675 ) -> None:
676 self.gpus[gpu_type] = power_data
679def num_gpus_to_str(
680 provision: dict[GPUType, int]
681) -> str:
682 return "+".join([
683 f"{num_gpus}x{gpu_type.name}"
684 for gpu_type, num_gpus in provision.items()
685 if num_gpus > 0
686 ])
689@dataclass
690class Provision:
691 num_gpus: dict[GPUType, int] = field(default_factory=dict)
693 def __getitem__(self, gpu_type: GPUType) -> int:
694 return self.num_gpus[gpu_type]
696 def __str__(self) -> str:
697 return num_gpus_to_str(self.num_gpus)
700@dataclass
701class ProvisioningResult:
702 latencies: list[float]
703 costs: list[float]
704 ttffs: list[float]
705 tbfs: list[float]
706 actual_provision: list[dict[GPUType, int]]
707 config_provision: list[dict[GPUType, int]]
708 model_provision: list[dict[GPUType, dict[Model, list[ModelAllocation]]]]
709 qualities: list[float] = field(default_factory=list)
710 energies: list[float] = field(default_factory=list)
712 def save(
713 self,
714 policy_name: str,
715 results_dir: str,
716 ) -> None:
717 """Save the provisioning results to a CSV file."""
718 num_a100: list[int] = []
719 num_h100: list[int] = []
720 num_h200: list[int] = []
721 num_gb200: list[int] = []
722 for provision in self.actual_provision:
723 num_a100.append(provision.get(GPUType.A100, 0))
724 num_h100.append(provision.get(GPUType.H100, 0))
725 num_h200.append(provision.get(GPUType.H200, 0))
726 num_gb200.append(provision.get(GPUType.GB200, 0))
727 df_latency = pd.DataFrame({
728 'num_a100': num_a100,
729 'num_h100': num_h100,
730 'num_h200': num_h200,
731 'num_gb200': num_gb200,
732 'ttff_s': self.ttffs,
733 'tbf_s': self.tbfs,
734 'cost': self.costs,
735 'total_time': self.latencies,
736 'energy': self.energies,
737 })
738 df_latency[['ttff_s', 'tbf_s', 'cost', 'total_time', 'energy']] = (
739 df_latency[['ttff_s', 'tbf_s', 'cost', 'total_time', 'energy']].round(2)
740 )
741 policy_name_clean = policy_name.replace(" ", "_").replace("*", "x").replace("/", "_").lower()
742 file_name = results_dir + f"provisioning_{policy_name_clean}.csv"
743 df_latency.to_csv(file_name, index=False)
745 def get_pareto_frontier(
746 self,
747 max_x: Optional[float] = None,
748 max_y: Optional[float] = None,
749 ) -> np.ndarray:
750 from utils import get_pareto_frontier # TODO this is a lazy fix, we need to reset
751 # points = np.array(list(zip(self.ttffs, self.costs)))
752 return get_pareto_frontier(
753 self.ttffs,
754 self.costs,
755 max_x=max_x,
756 max_y=max_y,
757 )
760class Solver(Enum):
761 GUROBI = "gurobi"
762 HIGHS = "highs"
763 GREEDY = "greedy"
764 NAIVE = "naive"
765 HEXGEN = "hexgen"
766 HELIX = "helix"
769@dataclass
770class Policy:
771 name: str
772 gpu_cost: dict[GPUType, float]
773 objective: Objective
774 disaggregation: dict[Model, bool]
775 use_upscaler: bool
776 hardware: list[GPUType] = field(default_factory=lambda: [GPUType.A100, GPUType.H100, GPUType.H200, GPUType.GB200])
777 solver: Solver = Solver.GREEDY
779 def is_disaggregated(self, model: Model) -> bool:
780 """Check if a model has disaggregation enabled."""
781 return self.disaggregation.get(model, False)
783 def __str__(self) -> str:
784 disag_str = {
785 model.value: disaggregated
786 for model, disaggregated in self.disaggregation.items()
787 if disaggregated
788 }
789 return (
790 f"Policy({self.name}, "
791 f"objective={self.objective}, "
792 f"disag={disag_str}, "
793 f"upscaler={self.use_upscaler}, "
794 f"cost={self.gpu_cost}, "
795 f"solver={self.solver})"
796 )