Coverage for streamwise/model_provisioner/naive_baseline.py: 97%

212 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-09 04:47 +0000

1""" 

2Naive baseline for the StreamWise workflow allocation problem. 

3""" 

4 

5from __future__ import annotations 

6 

7from typing import Optional 

8 

9from constants import NUM_GPUS_PER_SERVER 

10from constants import DEVICE_OPTIONS 

11 

12from sim_types import Result 

13from sim_types import GPUType 

14from sim_types import WorkflowConfig 

15from sim_types import LatencyData 

16from sim_types import PowerData 

17from sim_types import Policy 

18from sim_types import Solver 

19from sim_types import Model 

20from sim_types import ModelAllocation 

21from sim_types import Objective 

22 

23from models import FluxModelAllocation 

24from models import GemmaModelAllocation 

25from models import HFModelAllocation 

26from models import HFVAEModelAllocation 

27from models import FTModelAllocation 

28from models import FTVAEModelAllocation 

29from models import UpscalerModelAllocation 

30from models import OthersModelAllocation 

31 

32from evaluator import evaluate_model_allocation 

33 

34from .policies import NAIVE_POLICY 

35from .policies import MAX_DEVICES 

36 

37from model_allocator import ModelAllocator 

38 

39 

40class NaiveAllocator(ModelAllocator): 

41 """ 

42 Naive allocator that implements a simple heuristic. 

43 """ 

44 def __init__( 

45 self, 

46 workflow: WorkflowConfig, 

47 latency_data: LatencyData, 

48 power_data: Optional[PowerData] = None, 

49 policy: Policy = NAIVE_POLICY, 

50 ) -> None: 

51 super().__init__( 

52 workflow, 

53 latency_data, 

54 power_data, 

55 policy, 

56 ) 

57 assert self.policy.solver == Solver.NAIVE 

58 assert self.policy.objective == Objective.TTFF 

59 

60 def allocate( 

61 self, 

62 num_gpus: dict[GPUType, int], 

63 verbose: bool = False, 

64 ) -> Result: 

65 total_gpus = sum(num_gpus.values()) 

66 assert total_gpus >= 8, f"Total number of GPUs must be at least 8 ({num_gpus})" 

67 

68 gpu_types = [ 

69 gpu_type 

70 for gpu_type, count in num_gpus.items() 

71 if count > 0 

72 ] 

73 assert 1 <= len(gpu_types) <= 2, f"Only up to two GPU types are supported ({len(gpu_types)})" 

74 gpu_type1 = gpu_types[0] 

75 

76 if len(gpu_types) == 1: 

77 models = self._naive_single( 

78 num_gpus.get(gpu_type1, 0), 

79 gpu_type=gpu_type1, 

80 ) 

81 else: 

82 # Mixed setup of GPU types (e.g., A100 and H100) 

83 models = self._naive_two(num_gpus) 

84 

85 result = evaluate_model_allocation( 

86 models=models, 

87 num_gpus=num_gpus, 

88 workflow=self.workflow, 

89 latency_data=self.latency_data, 

90 power_data=self.power_data, 

91 policy=self.policy, 

92 round_up_cost_to_server=True, 

93 ) 

94 return result 

95 

96 def _naive_single( 

97 self, 

98 num_gpus: int, 

99 gpu_type: GPUType, 

100 ) -> dict[GPUType, dict[Model, list[ModelAllocation]]]: 

101 """Naive allocation for single GPU type.""" 

102 return self._naive_parallelism_allocation(gpu_type, num_gpus) 

103 

104 def _naive_two( 

105 self, 

106 num_gpus: dict[GPUType, int], 

107 ) -> dict[GPUType, dict[Model, list[ModelAllocation]]]: 

108 """Naive allocation for two GPU types.""" 

109 gpu_types = list(num_gpus.keys()) 

110 assert len(gpu_types) == 2 

111 assert len(num_gpus) == 2 

112 gpu_type1 = gpu_types[0] 

113 gpu_type2 = gpu_types[1] 

114 assert num_gpus[gpu_type1] >= NUM_GPUS_PER_SERVER[gpu_type1] 

115 assert num_gpus[gpu_type2] >= NUM_GPUS_PER_SERVER[gpu_type2] 

116 

117 # Initialize allocations with minimal setup 

118 models: dict[GPUType, dict[Model, list[ModelAllocation]]] = { 

119 gpu_type1: { # 3 x A100s (type1) 

120 Model.GEMMA: [GemmaModelAllocation( 

121 gpu_type=gpu_type1, 

122 devices=1, replicas=1)], 

123 Model.FLUX: [FluxModelAllocation( 

124 gpu_type=gpu_type1, 

125 devices=1, replicas=1)], 

126 Model.HF: [], 

127 Model.HF_VAE: [], 

128 Model.FT: [], 

129 Model.FT_VAE: [], 

130 Model.UPSCALER: [], 

131 Model.OTHERS: [OthersModelAllocation( 

132 gpu_type=gpu_type1, 

133 devices=1, replicas=1)], # + 1 for Kokoro/YOLO 

134 }, 

135 gpu_type2: { # 4 (+1) X H100 GPUs (type2) 

136 Model.GEMMA: [], 

137 Model.FLUX: [], 

138 Model.HF: [HFModelAllocation( 

139 gpu_type=gpu_type2, 

140 devices=1, replicas=1)], 

141 Model.HF_VAE: [HFVAEModelAllocation( 

142 gpu_type=gpu_type2, 

143 devices=1, replicas=1)], 

144 Model.FT: [FTModelAllocation( 

145 gpu_type=gpu_type2, 

146 devices=2, replicas=1)], 

147 Model.FT_VAE: [FTVAEModelAllocation( 

148 gpu_type=gpu_type2, 

149 devices=1, replicas=1)], 

150 Model.UPSCALER: [UpscalerModelAllocation( 

151 gpu_type=gpu_type2)], 

152 Model.OTHERS: [], 

153 }, 

154 } 

155 

156 # Calculate remaining: starting - assigned 

157 if not self.policy.is_disaggregated(Model.HF): 

158 models[gpu_type2][Model.HF][0].replicas = 2 

159 models[gpu_type2][Model.HF_VAE][0].replicas = 0 

160 if not self.policy.is_disaggregated(Model.FT): 

161 models[gpu_type2][Model.FT_VAE][0].replicas = 0 

162 

163 if self.policy.use_upscaler: 

164 models[gpu_type2][Model.UPSCALER][0].replicas = 1 

165 

166 models_gpu_type1 = self._naive_parallelism_allocation( 

167 gpu_type1, 

168 num_gpus.get(gpu_type1, 0), 

169 ) 

170 models_gpu_type2 = self._naive_parallelism_allocation( 

171 gpu_type2, 

172 num_gpus.get(gpu_type2, 0), 

173 # Already allocated in first GPU type 

174 skip_non_paralelizable_models=True, 

175 ) 

176 models[gpu_type1] = models_gpu_type1[gpu_type1] 

177 models[gpu_type2] = models_gpu_type2[gpu_type2] 

178 

179 # Apply per-GPU-type overrides after allocation 

180 if self.policy.use_upscaler: 

181 models[gpu_type2][Model.UPSCALER][0].replicas = 1 

182 

183 return models 

184 

185 def _naive_parallelism_allocation( 

186 self, 

187 gpu_type: GPUType, 

188 num_devices: int, 

189 skip_non_paralelizable_models: bool = False, 

190 ) -> dict[GPUType, dict[Model, list[ModelAllocation]]]: 

191 """ 

192 Device allocation for naive parallelism. 

193 Max devices for each model. 

194 Allocate devices to each model proportional to their max devices. 

195 """ 

196 models: dict[GPUType, dict[Model, list[ModelAllocation]]] = { 

197 gpu_type: { 

198 Model.GEMMA: [GemmaModelAllocation( 

199 gpu_type=gpu_type, 

200 replicas=1)], 

201 Model.FLUX: [FluxModelAllocation( 

202 gpu_type=gpu_type, 

203 replicas=1)], 

204 Model.HF: [HFModelAllocation( 

205 gpu_type=gpu_type, 

206 replicas=1)], 

207 Model.HF_VAE: [HFVAEModelAllocation( 

208 gpu_type=gpu_type, 

209 replicas=1 if self.policy.is_disaggregated(Model.HF) else 0)], 

210 Model.FT: [FTModelAllocation( 

211 gpu_type=gpu_type, 

212 replicas=4)], 

213 Model.FT_VAE: [FTVAEModelAllocation( 

214 gpu_type=gpu_type, 

215 replicas=1 if self.policy.is_disaggregated(Model.FT) else 0)], 

216 Model.OTHERS: [OthersModelAllocation( 

217 gpu_type=gpu_type, 

218 replicas=1)], # + 1 for Kokoro/YOLO 

219 Model.UPSCALER: [UpscalerModelAllocation( 

220 gpu_type=gpu_type, 

221 replicas=1 if self.policy.use_upscaler else 0)], 

222 }, 

223 } 

224 

225 # Zero out replicas for models not in workflow 

226 for model in Model: 

227 if model not in self.workflow.models: 

228 for alloc in models[gpu_type][model]: 

229 alloc.replicas = 0 

230 

231 # Zero out replicas for models that are not parallelizable when skip_non_paralelizable_models is True 

232 if skip_non_paralelizable_models: 

233 for model in Model: 

234 if not self.workflow.is_parallelizable(model): 

235 for alloc in models[gpu_type][model]: 

236 alloc.replicas = 0 

237 

238 # Assert only 1 allocation instance per model for naive parallelism 

239 for model in Model: 

240 assert len(models[gpu_type][model]) == 1, \ 

241 f"Expected only 1 allocation instance for {model}, got {len(models[gpu_type][model])}" 

242 

243 alloc_id = 0 

244 model_gemma = models[gpu_type][Model.GEMMA][alloc_id] 

245 model_flux = models[gpu_type][Model.FLUX][alloc_id] 

246 model_hf = models[gpu_type][Model.HF][alloc_id] 

247 model_vae = models[gpu_type][Model.HF_VAE][alloc_id] 

248 model_ft = models[gpu_type][Model.FT][alloc_id] 

249 model_ft_vae = models[gpu_type][Model.FT_VAE][alloc_id] 

250 model_upscaler = models[gpu_type][Model.UPSCALER][alloc_id] 

251 

252 # TODO do we need to do something for Model.OTHERS 

253 

254 if num_devices == 8: 

255 # single server case, use fixed allocation 

256 if Model.FT in self.workflow.models: 

257 model_ft.replicas = 4 

258 if self.policy.use_upscaler and Model.UPSCALER in self.workflow.models: 

259 model_upscaler.replicas = 1 

260 if Model.FT in self.workflow.models: 

261 model_ft.replicas -= 1 

262 if self.policy.is_disaggregated(Model.HF) and Model.HF_VAE in self.workflow.models: 

263 model_vae.replicas = 1 

264 if Model.FT in self.workflow.models: 

265 model_ft.replicas -= 1 

266 if self.policy.is_disaggregated(Model.FT) and Model.FT_VAE in self.workflow.models: 

267 model_ft_vae.replicas = 1 

268 if Model.FT in self.workflow.models: 

269 model_ft.replicas -= 1 

270 return models 

271 

272 init_num_devices = sum([ 

273 model[0].devices * model[0].replicas 

274 for model in models[gpu_type].values() 

275 ]) 

276 

277 # Allocate devices proportional to each model's max devices 

278 max_devices = MAX_DEVICES 

279 models_in_workflow = [ 

280 model 

281 for model in max_devices.keys() 

282 if model in self.workflow.models 

283 ] 

284 if skip_non_paralelizable_models: 

285 for model in max_devices.keys(): 

286 if not self.workflow.is_parallelizable(model): 

287 models_in_workflow.remove(model) 

288 

289 total_max_devices = sum([ 

290 max_devices[model] 

291 for model in models_in_workflow 

292 ]) 

293 for model in models_in_workflow: 

294 # Calculate the number of devices to allocate for the model, proportional to its max devices among others 

295 alloc_devices = int((num_devices - init_num_devices) * max_devices[model] / total_max_devices) 

296 if model == Model.GEMMA: 

297 max_devices_gemma = max_devices[Model.GEMMA] 

298 if self.latency_data: 

299 max_devices_gemma = min(max_devices_gemma, self.latency_data[gpu_type].get_max_parallelism(model)) 

300 model_gemma.devices += min(alloc_devices, max_devices_gemma) 

301 # Round down nearest in DEVICE_OPTIONS_GEMMA 

302 num_gemma_devices = max([ 

303 d 

304 for d in DEVICE_OPTIONS[Model.GEMMA] 

305 if d <= model_gemma.devices 

306 ]) 

307 model_gemma.devices = num_gemma_devices 

308 elif model == Model.FLUX: 

309 max_devices_flux = max_devices[Model.FLUX] 

310 if self.latency_data: 

311 max_devices_flux = min(max_devices_flux, self.latency_data[gpu_type].get_max_parallelism(model)) 

312 model_flux.devices += min(alloc_devices, max_devices_flux) 

313 # Round down nearest in DEVICE_OPTIONS_FLUX 

314 model_flux.devices = max([ 

315 d 

316 for d in DEVICE_OPTIONS[Model.FLUX] 

317 if d <= model_flux.devices 

318 ]) 

319 elif model == Model.HF: 

320 max_devices_hf = max_devices[Model.HF] 

321 if self.latency_data: 

322 max_devices_hf = min(max_devices_hf, self.latency_data[gpu_type].get_max_parallelism(model)) 

323 model_hf.replicas += min(alloc_devices, max_devices_hf) 

324 elif model == Model.HF_VAE: 

325 if self.policy.is_disaggregated(Model.HF): 

326 max_devices_vae = max_devices[Model.HF_VAE] 

327 if self.latency_data: 

328 max_devices_vae = min(max_devices_vae, self.latency_data[gpu_type].get_max_parallelism(model)) 

329 model_vae.replicas += min(alloc_devices, max_devices_vae) 

330 elif model == Model.FT: 

331 max_devices_ft = max_devices[Model.FT] 

332 if self.latency_data: 

333 max_devices_ft = min(max_devices_ft, self.latency_data[gpu_type].get_max_parallelism(model)) 

334 model_ft.replicas += min(alloc_devices, max_devices_ft) 

335 elif model == Model.FT_VAE: 

336 if self.policy.is_disaggregated(Model.FT): 

337 max_devices_ft_vae = max_devices[Model.FT_VAE] 

338 if self.latency_data: 

339 max_devices_ft_vae = min( 

340 max_devices_ft_vae, self.latency_data[gpu_type].get_max_parallelism(model) 

341 ) 

342 model_ft_vae.replicas += min(alloc_devices, max_devices_ft_vae) 

343 else: 

344 raise ValueError(f"Unrecognized model {model}") 

345 

346 remaining_devices = num_devices 

347 for model_name in models[gpu_type].keys(): 

348 for model_alloc in models[gpu_type][model_name]: 

349 remaining_devices -= model_alloc.get_num_gpus() 

350 

351 # Distribute remaining devices to parallelizable models 

352 distribute_models = self.workflow.filter_parallelizable_models( 

353 models_in_workflow, 

354 disaggregation=self.policy.disaggregation, 

355 ) 

356 # Prioritise models that already hold more GPUs 

357 distribute_models.sort( 

358 key=lambda m: models[gpu_type][m][alloc_id].get_num_gpus(), 

359 reverse=True, 

360 ) 

361 num_distribute = len(distribute_models) 

362 if num_distribute > 0 and remaining_devices > 0: 

363 made_progress = True 

364 while remaining_devices > 0 and made_progress: 

365 made_progress = False 

366 for model_name in distribute_models: 

367 gpus_per_replica = models[gpu_type][model_name][alloc_id].devices 

368 if gpus_per_replica <= 0 or remaining_devices < gpus_per_replica: 

369 continue 

370 models[gpu_type][model_name][alloc_id].replicas += 1 

371 remaining_devices -= gpus_per_replica 

372 made_progress = True 

373 if remaining_devices <= 0: 

374 break 

375 

376 remaining_devices = num_devices 

377 for model_name in models[gpu_type].keys(): 

378 for model_alloc in models[gpu_type][model_name]: 

379 remaining_devices -= model_alloc.get_num_gpus() 

380 

381 # TODO we should try to assign all resources 

382 # assert remaining_devices == 0, \ 

383 assert remaining_devices >= 0, \ 

384 f"remaining={remaining_devices} != 0: " \ 

385 f"gpu={gpu_type.value} total={num_devices} remaining={remaining_devices}" 

386 

387 # Update replicas based on total devices 

388 # Gemma (when parallelizable) 

389 if self.workflow.is_parallelizable(Model.GEMMA) and Model.GEMMA in models_in_workflow: 

390 model_gemma.devices, model_gemma.replicas, remaining_devices = _calculate_naive_num_devices( 

391 model_gemma.devices, 

392 model_gemma.replicas, 

393 remaining_devices, 

394 device_options=DEVICE_OPTIONS[Model.GEMMA], 

395 replica_upper_bound=self.workflow.total_scenes) 

396 

397 # Flux (when parallelizable) 

398 if self.workflow.is_parallelizable(Model.FLUX) and Model.FLUX in models_in_workflow: 

399 model_flux.devices, model_flux.replicas, remaining_devices = _calculate_naive_num_devices( 

400 model_flux.devices, 

401 model_flux.replicas, 

402 remaining_devices, 

403 device_options=DEVICE_OPTIONS[Model.FLUX], 

404 replica_upper_bound=self.workflow.total_scenes) 

405 

406 # Hunyuan FramePack 

407 if Model.HF in self.workflow.models: 

408 model_hf.devices, model_hf.replicas, remaining_devices = _calculate_naive_num_devices( 

409 model_hf.devices, 

410 model_hf.replicas, 

411 remaining_devices, 

412 device_options=DEVICE_OPTIONS[Model.HF], 

413 replica_upper_bound=self.workflow.total_scenes) 

414 

415 # Hunyuan FramePack VAE 

416 if self.policy.is_disaggregated(Model.HF) and Model.HF_VAE in self.workflow.models: 

417 model_vae.devices, model_vae.replicas, remaining_devices = _calculate_naive_num_devices( 

418 model_vae.devices, 

419 model_vae.replicas, 

420 remaining_devices, 

421 device_options=None, 

422 replica_upper_bound=self.workflow.total_frames[Model.HF], 

423 ) 

424 

425 # Fantasy Talking 

426 if Model.FT in self.workflow.models: 

427 model_ft.devices, model_ft.replicas, remaining_devices = _calculate_naive_num_devices( 

428 model_ft.devices, 

429 model_ft.replicas, 

430 remaining_devices, 

431 device_options=DEVICE_OPTIONS[Model.FT], 

432 replica_upper_bound=self.workflow.total_subscenes, 

433 ) 

434 

435 # Fantasy Talking VAE 

436 if self.policy.is_disaggregated(Model.FT) and Model.FT_VAE in self.workflow.models: 

437 model_ft_vae.devices, model_ft_vae.replicas, remaining_devices = _calculate_naive_num_devices( 

438 model_ft_vae.devices, 

439 model_ft_vae.replicas, 

440 remaining_devices, 

441 device_options=None, 

442 replica_upper_bound=self.workflow.total_frames[Model.FT], 

443 ) 

444 

445 return models 

446 

447 

448def _calculate_naive_num_devices( 

449 num_devices: int, 

450 num_replicas: int, 

451 remaining_devices: int, 

452 device_options: Optional[list[int]] = [1], 

453 replica_upper_bound: Optional[int] = None, 

454) -> tuple[int, int, int]: 

455 """Find the parallelism that maximizes the device usage.""" 

456 assert remaining_devices >= 0 

457 

458 model_quota = num_devices * num_replicas 

459 

460 if device_options: 

461 best_product = 0 

462 best_devices_per_replica = 1 

463 best_replicas = 1 

464 for devices_per_replica in device_options: 

465 if devices_per_replica > model_quota: 

466 continue 

467 max_replicas = model_quota // devices_per_replica 

468 if replica_upper_bound and max_replicas > replica_upper_bound: 

469 max_replicas = replica_upper_bound 

470 product = devices_per_replica * max_replicas 

471 if product > best_product: 

472 best_product = product 

473 best_devices_per_replica = devices_per_replica 

474 best_replicas = max_replicas 

475 else: 

476 # start with parallelism=1 instead 

477 best_devices_per_replica = 1 

478 best_replicas = model_quota 

479 

480 num_devices = best_devices_per_replica 

481 num_replicas = best_replicas 

482 remaining_devices += model_quota - num_replicas * num_devices 

483 

484 return num_devices, num_replicas, remaining_devices