Coverage for tests/simulator/test_models.py: 100%
220 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
1"""
2Direct unit tests for simulator/models.py.
3Tests the model allocation factory, helper functions, and per-model
4calculate_time / calculate_time_first / calculate_energy methods.
5"""
7from __future__ import annotations
9import sys
10import os
11import pytest
12from unittest.mock import patch
14# Add current path
15sys.path.append(os.getcwd())
17from tests.test_utils import temp_sys_path
19with temp_sys_path("simulator", "streamwise"):
20 from sim_types import GPUType
21 from sim_types import Model
22 from sim_types import ModelAllocation
23 from sim_types import QualityLevel
24 from sim_types import LatencyData
25 from sim_types import PowerData
27 from constants import DEFAULT_WORKFLOW_CONFIG
29 from data_loading import load_latency_data
30 from data_loading import load_power_data
32 from model_provisioner.policies import STREAMWISE_POLICY
33 from model_provisioner.policies import NAIVE_POLICY
35 from models import get_model_allocation
36 from models import _calculate_total_time
37 from models import assert_pixel_config
38 from models import _MODEL_ALLOCATION_REGISTRY
39 from models import GemmaModelAllocation
40 from models import FluxModelAllocation
41 from models import HFModelAllocation
42 from models import HFVAEModelAllocation
43 from models import FTModelAllocation
44 from models import FTVAEModelAllocation
45 from models import UpscalerModelAllocation
46 from models import OthersModelAllocation
49# ---------------------------------------------------------------------------
50# Helpers
51# ---------------------------------------------------------------------------
53def _make_latency_data() -> LatencyData:
54 """Return minimal LatencyData built from the real data files."""
55 return load_latency_data("simulator/data/")
58def _make_power_data() -> PowerData:
59 """Return minimal PowerData built from the real data files."""
60 return load_power_data("simulator/data/")
63# ---------------------------------------------------------------------------
64# get_model_allocation factory
65# ---------------------------------------------------------------------------
67def test_get_model_allocation_returns_correct_types() -> None:
68 """Factory returns the right ModelAllocation subclass for each Model."""
69 expected: list[tuple[Model, type[ModelAllocation]]] = [
70 (Model.GEMMA, GemmaModelAllocation),
71 (Model.FLUX, FluxModelAllocation),
72 (Model.HF, HFModelAllocation),
73 (Model.HF_VAE, HFVAEModelAllocation),
74 (Model.FT, FTModelAllocation),
75 (Model.FT_VAE, FTVAEModelAllocation),
76 (Model.UPSCALER, UpscalerModelAllocation),
77 (Model.OTHERS, OthersModelAllocation),
78 ]
79 for model, cls in expected:
80 alloc = get_model_allocation(
81 model=model,
82 gpu_type=GPUType.A100,
83 devices=1,
84 replicas=1,
85 )
86 assert isinstance(alloc, cls), f"Expected {cls.__name__} for {model}"
87 assert alloc.gpu_type == GPUType.A100
88 assert alloc.devices == 1
89 assert alloc.replicas == 1
92def test_get_model_allocation_zero_replicas() -> None:
93 """Factory creates an allocation with zero replicas (disabled) by default."""
94 alloc = get_model_allocation(
95 model=Model.FLUX,
96 gpu_type=GPUType.H100,
97 )
98 assert alloc.replicas == 0
99 assert alloc.get_num_gpus() == 0
102def test_get_model_allocation_unknown_model_raises() -> None:
103 """Factory raises ValueError for an unregistered Model value."""
104 # Temporarily remove GEMMA from the registry using patch.dict
105 with patch.dict(_MODEL_ALLOCATION_REGISTRY, {}, clear=False) as patched:
106 patched.pop(Model.GEMMA, None)
107 with pytest.raises(ValueError, match="No ModelAllocation for model"):
108 get_model_allocation(
109 model=Model.GEMMA,
110 gpu_type=GPUType.A100,
111 )
114# ---------------------------------------------------------------------------
115# _calculate_total_time
116# ---------------------------------------------------------------------------
118def test_calculate_total_time_zero_replicas() -> None:
119 """Zero replicas → zero time."""
120 assert _calculate_total_time(100.0, 0, 1.0) == 0.0
123def test_calculate_total_time_negative_replicas() -> None:
124 """Negative replicas → zero time."""
125 assert _calculate_total_time(100.0, -1, 1.0) == 0.0
128def test_calculate_total_time_single_replica() -> None:
129 """Single replica: total_work * time_per_work, clamped to time_per_work."""
130 # 10 work / 1 replica * 5.0 = 50.0
131 assert _calculate_total_time(10.0, 1, 5.0) == 50.0
134def test_calculate_total_time_floor_at_single_work_unit() -> None:
135 """Time cannot be less than time_per_work (single unit floor)."""
136 # 1 work, 10 replicas → 1/10 * 5.0 = 0.5, floored to 5.0
137 assert _calculate_total_time(1.0, 10, 5.0) == 5.0
140def test_calculate_total_time_many_replicas() -> None:
141 """Many replicas reduce time proportionally."""
142 # 20/4 * 2.0 = 10.0; 10.0 > time_per_work(2.0) → 10.0
143 assert _calculate_total_time(20.0, 4, 2.0) == 10.0
146# ---------------------------------------------------------------------------
147# assert_pixel_config
148# ---------------------------------------------------------------------------
150def test_assert_pixel_config() -> None:
151 """assert_pixel_config passes for valid config and raises for invalid."""
152 assert_pixel_config(DEFAULT_WORKFLOW_CONFIG)
154 # Patching MEDIUM > HIGH violates the ordering constraint → AssertionError.
155 with patch.dict("sim_types.RESOLUTION_PIXELS",
156 {QualityLevel.MEDIUM: 1000, QualityLevel.HIGH: 500}):
157 with pytest.raises(AssertionError):
158 assert_pixel_config(DEFAULT_WORKFLOW_CONFIG)
161# ---------------------------------------------------------------------------
162# Zero-GPU paths (replicas=0 → time=0, energy=0)
163# All models must return 0.0 when no GPUs are allocated.
164# ---------------------------------------------------------------------------
166def test_zero_gpu_all_models() -> None:
167 latency_data = _make_latency_data()
168 power_data = _make_power_data()
170 zero_allocs = [
171 GemmaModelAllocation(gpu_type=GPUType.A100), # replicas=0 by default
172 FluxModelAllocation(gpu_type=GPUType.A100),
173 HFModelAllocation(gpu_type=GPUType.A100),
174 HFVAEModelAllocation(gpu_type=GPUType.A100),
175 FTModelAllocation(gpu_type=GPUType.A100),
176 FTVAEModelAllocation(gpu_type=GPUType.A100),
177 UpscalerModelAllocation(gpu_type=GPUType.A100),
178 OthersModelAllocation(gpu_type=GPUType.A100),
179 ]
180 workflow = DEFAULT_WORKFLOW_CONFIG
181 for alloc in zero_allocs:
182 assert alloc.get_num_gpus() == 0
183 t = alloc.calculate_time(STREAMWISE_POLICY, workflow, latency_data)
184 assert t == 0.0, f"{type(alloc).__name__}.calculate_time with 0 GPUs should be 0"
185 tf = alloc.calculate_time_first(STREAMWISE_POLICY, workflow, latency_data)
186 assert tf == 0.0, f"{type(alloc).__name__}.calculate_time_first with 0 GPUs should be 0"
187 e = alloc.calculate_energy(workflow, power_data, total_time_s=1000.0)
188 assert e == 0.0, f"{type(alloc).__name__}.calculate_energy with 0 GPUs should be 0"
191# ---------------------------------------------------------------------------
192# GemmaModelAllocation
193# ---------------------------------------------------------------------------
195def test_gemma_calculate_time_single_gpu() -> None:
196 """Gemma time with 1 GPU should be positive and scale with tokens."""
197 latency_data = _make_latency_data()
198 alloc = GemmaModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=1)
199 t = alloc.calculate_time(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
200 assert t > 0.0
201 assert alloc.time == t
204def test_gemma_calculate_time_first_single_gpu() -> None:
205 """Gemma TTFF < total time."""
206 latency_data = _make_latency_data()
207 alloc = GemmaModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=1)
208 alloc.calculate_time(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
209 tf = alloc.calculate_time_first(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
210 assert 0.0 < tf <= alloc.time
213def test_gemma_calculate_energy() -> None:
214 """Gemma energy > 0 when power data is provided."""
215 latency_data = _make_latency_data()
216 power_data = _make_power_data()
217 alloc = GemmaModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=1)
218 alloc.calculate_time(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
219 alloc.calculate_time_first(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
220 e = alloc.calculate_energy(
221 DEFAULT_WORKFLOW_CONFIG,
222 power_data=power_data,
223 total_time_s=alloc.time * 2,
224 )
225 assert e > 0.0
228def test_gemma_calculate_energy_no_power_data() -> None:
229 """Energy is 0 when no power data is provided."""
230 latency_data = _make_latency_data()
231 alloc = GemmaModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=1)
232 alloc.calculate_time(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
233 e = alloc.calculate_energy(DEFAULT_WORKFLOW_CONFIG, power_data=None)
234 assert e == 0.0
237def test_gemma_get_max_replicas() -> None:
238 """Gemma max replicas equals model_work.get(GEMMA, 1)."""
239 alloc = GemmaModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=1)
240 max_r = alloc.get_max_replicas(DEFAULT_WORKFLOW_CONFIG)
241 assert max_r == DEFAULT_WORKFLOW_CONFIG.model_work.get(Model.GEMMA, 1)
244# ---------------------------------------------------------------------------
245# FluxModelAllocation
246# ---------------------------------------------------------------------------
248def test_flux_calculate_time_single_gpu() -> None:
249 """Flux time with 1 GPU should be positive."""
250 latency_data = _make_latency_data()
251 alloc = FluxModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=1)
252 t = alloc.calculate_time(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
253 assert t > 0.0
256def test_flux_time_equals_time_first() -> None:
257 """For Flux, time and time_first are the same (single-scene model)."""
258 latency_data = _make_latency_data()
259 alloc = FluxModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=1)
260 t = alloc.calculate_time(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
261 tf = alloc.calculate_time_first(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
262 assert abs(t - tf) < 1e-6
265def test_flux_multi_device_faster() -> None:
266 """Flux with more GPU devices should be faster (or equal)."""
267 latency_data = _make_latency_data()
268 alloc1 = FluxModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=1)
269 alloc2 = FluxModelAllocation(gpu_type=GPUType.A100, devices=2, replicas=1)
270 t1 = alloc1.calculate_time(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
271 t2 = alloc2.calculate_time(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
272 assert t2 <= t1
275# ---------------------------------------------------------------------------
276# HFModelAllocation
277# ---------------------------------------------------------------------------
279def test_hf_calculate_time_single_gpu() -> None:
280 """HF time with 1 GPU should be positive."""
281 latency_data = _make_latency_data()
282 alloc = HFModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=1)
283 t = alloc.calculate_time(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
284 assert t > 0.0
287def test_hf_more_replicas_reduces_time() -> None:
288 """More HF replicas should reduce total time."""
289 latency_data = _make_latency_data()
290 alloc1 = HFModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=1)
291 alloc4 = HFModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=4)
292 t1 = alloc1.calculate_time(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
293 t4 = alloc4.calculate_time(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
294 assert t4 < t1
297def test_hf_get_max_replicas() -> None:
298 alloc = HFModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=1)
299 max_r = alloc.get_max_replicas(DEFAULT_WORKFLOW_CONFIG)
300 assert max_r == DEFAULT_WORKFLOW_CONFIG.model_work.get(Model.HF, 1)
303# ---------------------------------------------------------------------------
304# FTModelAllocation
305# ---------------------------------------------------------------------------
307def test_ft_calculate_time_single_gpu() -> None:
308 """FT time with 1 GPU should be positive."""
309 latency_data = _make_latency_data()
310 alloc = FTModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=1)
311 t = alloc.calculate_time(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
312 assert t > 0.0
315def test_ft_time_first_equals_one_subscene() -> None:
316 """FT time_first corresponds to a single subscene duration."""
317 latency_data = _make_latency_data()
318 alloc = FTModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=1)
319 alloc.calculate_time(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
320 tf = alloc.calculate_time_first(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
321 assert 0.0 < tf <= alloc.time
324def test_ft_get_max_replicas() -> None:
325 alloc = FTModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=1)
326 max_r = alloc.get_max_replicas(DEFAULT_WORKFLOW_CONFIG)
327 assert max_r == DEFAULT_WORKFLOW_CONFIG.model_work.get(Model.FT, 1)
330# ---------------------------------------------------------------------------
331# HFVAEModelAllocation (disaggregated)
332# ---------------------------------------------------------------------------
334def test_hf_vae_disabled_when_not_disaggregated() -> None:
335 """HF_VAE with no disaggregation (NAIVE_POLICY) should stay at zero."""
336 # NAIVE_POLICY has disaggregation={} → HF not disaggregated
337 alloc = HFVAEModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=0)
338 latency_data = _make_latency_data()
339 t = alloc.calculate_time(NAIVE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
340 assert t == 0.0
343def test_hf_vae_positive_when_disaggregated() -> None:
344 """HF_VAE with disaggregation enabled should give positive time."""
345 alloc = HFVAEModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=1)
346 latency_data = _make_latency_data()
347 t = alloc.calculate_time(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
348 assert t > 0.0
351# ---------------------------------------------------------------------------
352# FTVAEModelAllocation (disaggregated)
353# ---------------------------------------------------------------------------
355def test_ft_vae_disabled_when_not_disaggregated() -> None:
356 """FT_VAE with no disaggregation should stay at zero."""
357 alloc = FTVAEModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=0)
358 latency_data = _make_latency_data()
359 t = alloc.calculate_time(NAIVE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
360 assert t == 0.0
363# ---------------------------------------------------------------------------
364# UpscalerModelAllocation
365# ---------------------------------------------------------------------------
367def test_upscaler_calculate_time() -> None:
368 """Upscaler time should be positive with 1 replica."""
369 latency_data = _make_latency_data()
370 alloc = UpscalerModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=1)
371 t = alloc.calculate_time(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
372 assert t > 0.0
375def test_upscaler_time_first() -> None:
376 """Upscaler time_first should be positive when use_upscaler=True."""
377 latency_data = _make_latency_data()
378 alloc = UpscalerModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=1)
379 tf = alloc.calculate_time_first(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
380 assert tf > 0.0
383def test_upscaler_get_max_replicas() -> None:
384 alloc = UpscalerModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=1)
385 max_r = alloc.get_max_replicas(DEFAULT_WORKFLOW_CONFIG)
386 assert max_r == DEFAULT_WORKFLOW_CONFIG.model_work.get(Model.UPSCALER, 1)
389# ---------------------------------------------------------------------------
390# OthersModelAllocation
391# ---------------------------------------------------------------------------
393def test_others_calculate_time() -> None:
394 """Others time is proportional to total_scenes."""
395 latency_data = _make_latency_data()
396 alloc = OthersModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=1)
397 t = alloc.calculate_time(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
398 assert t > 0.0
401def test_others_time_first_less_than_total() -> None:
402 """Others time_first equals latency for a single scene."""
403 latency_data = _make_latency_data()
404 alloc = OthersModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=1)
405 alloc.calculate_time(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
406 tf = alloc.calculate_time_first(STREAMWISE_POLICY, DEFAULT_WORKFLOW_CONFIG, latency_data)
407 assert 0.0 < tf <= alloc.time
410# ---------------------------------------------------------------------------
411# ModelAllocation.calculate() convenience method
412# ---------------------------------------------------------------------------
414def test_calculate_convenience_method_populates_all_fields() -> None:
415 """ModelAllocation.calculate() should fill time, time_first, cost, energy."""
416 latency_data = _make_latency_data()
417 power_data = _make_power_data()
418 alloc = FluxModelAllocation(gpu_type=GPUType.A100, devices=1, replicas=1)
419 alloc.calculate(
420 policy=STREAMWISE_POLICY,
421 workflow=DEFAULT_WORKFLOW_CONFIG,
422 latency_data=latency_data,
423 power_data=power_data,
424 total_time_s=100.0,
425 )
426 assert alloc.time > 0.0
427 assert alloc.time_first > 0.0
428 assert alloc.cost > 0.0
429 assert alloc.energy > 0.0
432# ---------------------------------------------------------------------------
433# ModelAllocation.disable()
434# ---------------------------------------------------------------------------
436def test_disable_zeroes_allocation() -> None:
437 """disable() should zero all fields."""
438 alloc = FluxModelAllocation(gpu_type=GPUType.A100, devices=2, replicas=2)
439 alloc.time = 5.0
440 alloc.time_first = 1.0
441 alloc.energy = 100.0
442 alloc.disable()
443 assert alloc.devices == 0
444 assert alloc.replicas == 0
445 assert alloc.time == 0.0
446 assert alloc.time_first == 0.0
447 assert alloc.energy == 0.0
448 assert alloc.get_num_gpus() == 0