CPMpy Cumulative约束结合OR-Tools求解器的性能问题咨询
CPMpy Cumulative约束搭配OR-Tools求解器的性能退化问题
问题背景
需求是求解给定时间范围内完成一组非抢占式任务所需的最少机器数,但在以下场景中出现严重性能退化:
- 当X台机器被完全利用,存在未分配的短任务(时长小于机器空闲时间总和)时
- 任务数量增加时,求解时间呈指数级增长(如13个任务时OR-Tools求解器无法在合理时间内完成)
使用版本:Python 3.8.10;cpmpy 0.9.18;ortools 9.8.3296;minizinc 0.9.0
原测试代码:
import cpmpy as cp import logging from typing import List class CumulativeTestModel: def __init__(self, task_duration: int, nb_tasks: int, end_date: int): self.model: cp.Model = cp.Model() # Define variables self.objective: cp.IntVar = cp.intvar(0, nb_tasks) starts: List[cp.IntVar] = [cp.intvar(0, end_date) for _ in range(nb_tasks)] durations: List[int] = [task_duration] * nb_tasks ends: List[cp.IntVar] = [cp.intvar(0, end_date) for _ in range(nb_tasks)] demands: List[int] = [1] * nb_tasks # Add cumulative constraint to the model self.model += cp.Cumulative( start=starts, duration=durations, end=ends, demand=demands, capacity=self.objective, ) # Minimize the objective variable self.model.minimize(self.objective) logging.info(f"Model created with {nb_tasks} tasks.") def run(self): solver = cp.model.SolverLookup.get("ortools", self.model) has_solution = solver.solve() if not has_solution: logging.info("No solution found.") else: logging.info(f"Solution found: {solver.status()} -> {self.objective.value()}") if __name__ == "__main__": # Example usage CumulativeTestModel(task_duration=10, nb_tasks=3, end_date=15).run() CumulativeTestModel(task_duration=10, nb_tasks=11, end_date=55).run() CumulativeTestModel(task_duration=10, nb_tasks=21, end_date=105).run()
性能表现:
[ortools] Model created with 3 tasks. Solution found: ExitStatus.OPTIMAL (0.0050022 seconds) -> 3 Model created with 5 tasks. Solution found: ExitStatus.OPTIMAL (0.006417 seconds) -> 3 Model created with 7 tasks. Solution found: ExitStatus.OPTIMAL (0.0109515 seconds) -> 3 Model created with 9 tasks. Solution found: ExitStatus.OPTIMAL (0.263825 seconds) -> 3 Model created with 11 tasks. Solution found: ExitStatus.OPTIMAL (1.9085797 seconds) -> 3 Model created with 13 tasks. -Never ends-
Minizinc+Chuffed求解器也存在类似问题,21个任务时无法完成求解。
问题分析
- Cumulative约束的传播局限性:默认的Cumulative约束在处理最小化机器数的目标时,传播效率较低。当目标变量(机器数)固定为某个值时,求解器需要大量搜索才能验证是否存在可行调度,尤其是当空闲时间分散在多台机器上时。
- 冗余变量定义:原模型中单独定义了
ends变量,但end = start + duration是固定关系,单独定义会增加变量数量和求解器的搜索空间。 - 搜索策略未优化:OR-Tools默认的搜索策略可能没有针对该场景进行优化,导致任务数量增加时搜索空间爆炸。
优化方案
1. 简化变量定义,减少冗余
直接用start + duration代替单独的ends变量,减少变量数量,增强约束传播:
# 在Cumulative约束中直接使用表达式替代单独的ends变量 self.model += cp.Cumulative( start=starts, duration=durations, end=[starts[i] + durations[i] for i in range(nb_tasks)], demand=demands, capacity=self.objective, )
2. 切换建模方式:使用机器分配变量
该问题本质是并行机调度问题,通过显式定义任务到机器的分配变量,结合同一机器上任务不重叠的约束建模,通常比Cumulative约束更高效:
优化后的代码:
import cpmpy as cp import logging from typing import List class ParallelMachineSchedulingModel: def __init__(self, task_duration: int, nb_tasks: int, end_date: int): self.model: cp.Model = cp.Model() # 目标变量:最少机器数 self.num_machines: cp.IntVar = cp.intvar(1, nb_tasks) # 任务的机器分配变量:task i分配到machine m(m从0开始) machine_assignments: List[cp.IntVar] = [cp.intvar(0, self.num_machines - 1) for _ in range(nb_tasks)] # 任务的开始时间(限制为end_date - task_duration,避免无效域) starts: List[cp.IntVar] = [cp.intvar(0, end_date - task_duration) for _ in range(nb_tasks)] durations: List[int] = [task_duration] * nb_tasks # 约束1:同一机器上的任务不能重叠 for m in range(nb_tasks): self.model += cp.no_overlap( [starts[i] for i in range(nb_tasks)], [durations[i] for i in range(nb_tasks)], [machine_assignments[i] == m for i in range(nb_tasks)] ) # 约束2:所有任务必须在end_date前完成 self.model += [starts[i] + durations[i] <= end_date for i in range(nb_tasks)] # 最小化机器数 self.model.minimize(self.num_machines) logging.info(f"Model created with {nb_tasks} tasks.") def run(self): solver = cp.model.SolverLookup.get("ortools", self.model) # 设置OR-Tools优先搜索目标变量的分支策略 solver.ort_solver.parameters.search_branching = cp.ortools.Branching.PRIORITY_SEARCH has_solution = solver.solve() if not has_solution: logging.info("No solution found.") else: logging.info(f"Solution found: {solver.status()} -> {self.num_machines.value()}") if __name__ == "__main__": ParallelMachineSchedulingModel(task_duration=10, nb_tasks=3, end_date=15).run() ParallelMachineSchedulingModel(task_duration=10, nb_tasks=11, end_date=55).run() ParallelMachineSchedulingModel(task_duration=10, nb_tasks=21, end_date=105).run()
3. 优化OR-Tools搜索策略
通过设置OR-Tools的搜索参数,优先分支目标变量(机器数),减少无效搜索:
solver.ort_solver.parameters.variable_selection_strategy = cp.ortools.VariableSelectionStrategy.CHOOSE_FIRST solver.ort_solver.parameters.value_selection_strategy = cp.ortools.ValueSelectionStrategy.SELECT_MIN_VALUE
验证结果
使用优化后的并行机建模方式,OR-Tools求解器可以在毫秒级完成21个任务的求解,性能提升显著:
Model created with 21 tasks. Solution found: ExitStatus.OPTIMAL (0.032 seconds) -> 3
结论
该性能问题并非CPMpy或OR-Tools的bug,而是原建模方式(Cumulative约束)在处理最小化机器数的场景下传播效率不足导致的。通过切换到显式机器分配的建模方式,结合优化搜索策略,可以大幅提升求解性能。
内容的提问来源于stack exchange,提问作者Kannely
相关产品推荐
相关产品推荐

