Skip to content

Commit c19c9a3

Browse files
committed
Implements a simple concurrent solver for highspy, that launches several threads with different random seeds and syncronizes the best solution via callbacks.
Initial prototype, need to add tests, documentation etc.
1 parent 159daf4 commit c19c9a3

File tree

1 file changed

+84
-10
lines changed

1 file changed

+84
-10
lines changed

highs/highspy/highs.py

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from numbers import Integral
55
from itertools import product
66
from threading import Thread, local, RLock, Lock
7+
from multiprocessing import cpu_count
78
from typing import Optional, Any, overload, Callable, Sequence, Mapping, Iterable, SupportsIndex, cast, Union
89

910
from ._core import (
@@ -29,17 +30,19 @@ class Highs(_Highs):
2930
HiGHS solver interface
3031
"""
3132

32-
__handle_keyboard_interrupt: bool = False
33-
__handle_user_interrupt: bool = False
34-
__solver_should_stop: bool = False
35-
__solver_stopped: RLock = RLock()
36-
__solver_started: Lock = Lock()
37-
__solver_status: Optional[HighsStatus] = None
38-
3933
def __init__(self):
4034
super().__init__()
4135
self.callbacks = [HighsCallback(cb.HighsCallbackType(_), self) for _ in range(int(cb.HighsCallbackType.kCallbackMax) + 1)]
4236
self.enableCallbacks()
37+
38+
self.__handle_keyboard_interrupt: bool = False
39+
self.__handle_user_interrupt: bool = False
40+
self.__use_concurrent_solve: bool = False
41+
42+
self.__solver_should_stop: bool = False
43+
self.__solver_stopped: RLock = RLock()
44+
self.__solver_started: Lock = Lock()
45+
self.__solver_status: Optional[HighsStatus] = None
4346

4447
# Silence logging
4548
def silent(self, turn_off_output: bool = True):
@@ -55,10 +58,70 @@ def solve(self):
5558
Returns:
5659
A HighsStatus object containing the solve status.
5760
"""
58-
if not self.HandleKeyboardInterrupt:
59-
return super().run()
61+
if not self.ConcurrentSolve:
62+
if not self.HandleKeyboardInterrupt:
63+
return super().run()
64+
else:
65+
return self.joinSolve(self.startSolve())
6066
else:
61-
return self.joinSolve(self.startSolve())
67+
num_threads = self.getOptionValue("threads")
68+
if num_threads == 0:
69+
num_threads = int(cpu_count() / 2)
70+
71+
clones = [self] + [Highs() for _ in range(num_threads - 1)]
72+
73+
__best_solution = Lock()
74+
__best = [None, np.zeros(self.getNumCol())] # objective, solution
75+
76+
if self.getObjectiveSense()[1] == ObjSense.kMinimize:
77+
is_better = lambda a,b: a < b
78+
current_objective = [self.inf] * num_threads
79+
else:
80+
is_better = lambda a,b: a > b
81+
current_objective = [-self.inf] * num_threads
82+
83+
def get_solution(e):
84+
current_objective[int(e.user_data)] = e.data_out.objective_function_value
85+
86+
with __best_solution:
87+
if __best[0] == None or is_better(e.data_out.objective_function_value, __best[0]):
88+
__best[0] = e.data_out.objective_function_value
89+
__best[1][:] = e.data_out.mip_solution
90+
# print("Better incumbent found", int(e.user_data), __best[0])
91+
92+
def put_solution(e):
93+
with __best_solution:
94+
if __best[0] != None and is_better(__best[0], current_objective[int(e.user_data)]):
95+
# print("Updating thread", int(e.user_data), __best[0], current_objective[int(e.user_data)])
96+
e.data_in.user_has_solution = True
97+
e.data_in.user_solution[:] = __best[1]
98+
current_objective[int(e.user_data)] = __best[0]
99+
100+
clones[0].cbMipImprovingSolution.subscribe(get_solution, 0)
101+
clones[0].cbMipUserSolution.subscribe(put_solution, 0)
102+
clones[0].HandleUserInterrupt = True
103+
104+
for i in range(1, num_threads):
105+
clones[i].silent()
106+
clones[i].setOptionValue("random_seed", i)
107+
clones[i].HandleUserInterrupt = True
108+
clones[i].passModel(self.getModel())
109+
clones[i].cbMipImprovingSolution.subscribe(get_solution, i)
110+
clones[i].cbMipUserSolution.subscribe(put_solution, i)
111+
112+
threads = []
113+
114+
for i in range(num_threads):
115+
threads.append(clones[i].startSolve())
116+
117+
clones[0].joinSolve(threads[0])
118+
clones[0].cbMipImprovingSolution.unsubscribe(get_solution)
119+
clones[0].cbMipUserSolution.unsubscribe(put_solution)
120+
121+
for i in range(1, num_threads):
122+
clones[i].cancelSolve()
123+
clones[i].joinSolve(threads[i])
124+
62125

63126
def startSolve(self):
64127
"""
@@ -1271,6 +1334,17 @@ def __user_interrupt_event(self, e: HighsCallbackEvent):
12711334
if self.__solver_should_stop:
12721335
e.interrupt()
12731336

1337+
@property
1338+
def ConcurrentSolve(self):
1339+
"""
1340+
Get/Set whether the solver should run on separate threads with different random seeds
1341+
"""
1342+
return self.__use_concurrent_solve
1343+
1344+
@ConcurrentSolve.setter
1345+
def ConcurrentSolve(self, value: bool):
1346+
self.__use_concurrent_solve = value
1347+
12741348
@property
12751349
def cbLogging(self):
12761350
return self.callbacks[int(cb.HighsCallbackType.kCallbackLogging)]

0 commit comments

Comments
 (0)