Skip to content

Commit

Permalink
solve problem with multi-threads (#918)
Browse files Browse the repository at this point in the history
* 1. model optimize without gil
2. solveFirstInterruptOthers
3. add copy model method
4. add copy model solution
5. add some Solution methods

* 1.  model optimize without gil
2.  model addOrigVarsConssObjectiveFrom
3.  add some Solution methods

* Update src/pyscipopt/scip.pxi

Co-authored-by: João Dionísio <[email protected]>

* remove addOrigVarsConssObjectiveFrom

* rename getSolOrigin to getOrigin

---------

Co-authored-by: Light1_Lee <[email protected]>
Co-authored-by: João Dionísio <[email protected]>
  • Loading branch information
3 people authored Nov 18, 2024
1 parent c24e9df commit 6876af9
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 2 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
- Added additional tests to test_nodesel, test_heur, and test_strong_branching
- Migrated documentation to Readthedocs
- `attachEventHandlerCallback` method to Model for a more ergonomic way to attach event handlers
- Added Model method: optimizeNogil
- Added Solution method: getOrigin, retransform, translate
- Added SCIP.pxd: SCIP_SOLORIGIN, SCIPcopyOrigVars, SCIPcopyOrigConss, SCIPsolve nogil, SCIPretransformSol, SCIPtranslateSubSol, SCIPsolGetOrigin, SCIPhashmapCreate, SCIPhashmapFree
- Added additional tests to test_multi_threads, test_solution, and test_copy
### Fixed
- Fixed too strict getObjVal, getVal check
### Changed
Expand Down
1 change: 1 addition & 0 deletions src/pyscipopt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@
from pyscipopt.scip import PY_SCIP_BRANCHDIR as SCIP_BRANCHDIR
from pyscipopt.scip import PY_SCIP_BENDERSENFOTYPE as SCIP_BENDERSENFOTYPE
from pyscipopt.scip import PY_SCIP_ROWORIGINTYPE as SCIP_ROWORIGINTYPE
from pyscipopt.scip import PY_SCIP_SOLORIGIN as SCIP_SOLORIGIN
23 changes: 22 additions & 1 deletion src/pyscipopt/scip.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,17 @@ cdef extern from "scip/scip.h":
SCIP_ROWORIGINTYPE SCIP_ROWORIGINTYPE_SEPA
SCIP_ROWORIGINTYPE SCIP_ROWORIGINTYPE_REOPT

ctypedef int SCIP_SOLORIGIN
cdef extern from "scip/type_sol.h":
SCIP_SOLORIGIN SCIP_SOLORIGIN_ORIGINAL
SCIP_SOLORIGIN SCIP_SOLORIGIN_ZERO
SCIP_SOLORIGIN SCIP_SOLORIGIN_LPSOL
SCIP_SOLORIGIN SCIP_SOLORIGIN_NLPSOL
SCIP_SOLORIGIN SCIP_SOLORIGIN_RELAXSOL
SCIP_SOLORIGIN SCIP_SOLORIGIN_PSEUDOSOL
SCIP_SOLORIGIN SCIP_SOLORIGIN_PARTIAL
SCIP_SOLORIGIN SCIP_SOLORIGIN_UNKNOWN

ctypedef bint SCIP_Bool

ctypedef long long SCIP_Longint
Expand Down Expand Up @@ -532,6 +543,8 @@ cdef extern from "scip/scip.h":
SCIP_Bool threadsafe,
SCIP_Bool passmessagehdlr,
SCIP_Bool* valid)
SCIP_RETCODE SCIPcopyOrigVars(SCIP* sourcescip, SCIP* targetscip, SCIP_HASHMAP* varmap, SCIP_HASHMAP* consmap, SCIP_VAR** fixedvars, SCIP_Real* fixedvals, int nfixedvars )
SCIP_RETCODE SCIPcopyOrigConss(SCIP* sourcescip, SCIP* targetscip, SCIP_HASHMAP* varmap, SCIP_HASHMAP* consmap, SCIP_Bool enablepricing, SCIP_Bool* valid)
SCIP_RETCODE SCIPmessagehdlrCreate(SCIP_MESSAGEHDLR **messagehdlr,
SCIP_Bool bufferedoutput,
const char *filename,
Expand Down Expand Up @@ -669,6 +682,7 @@ cdef extern from "scip/scip.h":

# Solve Methods
SCIP_RETCODE SCIPsolve(SCIP* scip)
SCIP_RETCODE SCIPsolve(SCIP* scip) noexcept nogil
SCIP_RETCODE SCIPsolveConcurrent(SCIP* scip)
SCIP_RETCODE SCIPfreeTransform(SCIP* scip)
SCIP_RETCODE SCIPpresolve(SCIP* scip)
Expand Down Expand Up @@ -871,7 +885,9 @@ cdef extern from "scip/scip.h":
SCIP_RETCODE SCIPreadSolFile(SCIP* scip, const char* filename, SCIP_SOL* sol, SCIP_Bool xml, SCIP_Bool* partial, SCIP_Bool* error)
SCIP_RETCODE SCIPcheckSol(SCIP* scip, SCIP_SOL* sol, SCIP_Bool printreason, SCIP_Bool completely, SCIP_Bool checkbounds, SCIP_Bool checkintegrality, SCIP_Bool checklprows, SCIP_Bool* feasible)
SCIP_RETCODE SCIPcheckSolOrig(SCIP* scip, SCIP_SOL* sol, SCIP_Bool* feasible, SCIP_Bool printreason, SCIP_Bool completely)

SCIP_RETCODE SCIPretransformSol(SCIP* scip, SCIP_SOL* sol)
SCIP_RETCODE SCIPtranslateSubSol(SCIP* scip, SCIP* subscip, SCIP_SOL* subsol, SCIP_HEUR* heur, SCIP_VAR** subvars, SCIP_SOL** newsol)
SCIP_SOLORIGIN SCIPsolGetOrigin(SCIP_SOL* sol)
SCIP_Real SCIPgetSolTime(SCIP* scip, SCIP_SOL* sol)

SCIP_RETCODE SCIPsetRelaxSolVal(SCIP* scip, SCIP_RELAX* relax, SCIP_VAR* var, SCIP_Real val)
Expand Down Expand Up @@ -1367,6 +1383,11 @@ cdef extern from "scip/scip.h":

BMS_BLKMEM* SCIPblkmem(SCIP* scip)

# pub_misc.h
SCIP_RETCODE SCIPhashmapCreate(SCIP_HASHMAP** hashmap, BMS_BLKMEM* blkmem, int mapsize)
void SCIPhashmapFree(SCIP_HASHMAP** hashmap)


cdef extern from "scip/tree.h":
int SCIPnodeGetNAddedConss(SCIP_NODE* node)

Expand Down
52 changes: 52 additions & 0 deletions src/pyscipopt/scip.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,16 @@ cdef class PY_SCIP_ROWORIGINTYPE:
SEPA = SCIP_ROWORIGINTYPE_SEPA
REOPT = SCIP_ROWORIGINTYPE_REOPT

cdef class PY_SCIP_SOLORIGIN:
ORIGINAL = SCIP_SOLORIGIN_ORIGINAL
ZERO = SCIP_SOLORIGIN_ZERO
LPSOL = SCIP_SOLORIGIN_LPSOL
NLPSOL = SCIP_SOLORIGIN_NLPSOL
RELAXSOL = SCIP_SOLORIGIN_RELAXSOL
PSEUDOSOL = SCIP_SOLORIGIN_PSEUDOSOL
PARTIAL = SCIP_SOLORIGIN_PARTIAL
UNKNOWN = SCIP_SOLORIGIN_UNKNOWN

def PY_SCIP_CALL(SCIP_RETCODE rc):
if rc == SCIP_OKAY:
pass
Expand Down Expand Up @@ -1009,6 +1019,40 @@ cdef class Solution:
if not stage_check or self.sol == NULL and SCIPgetStage(self.scip) != SCIP_STAGE_SOLVING:
raise Warning(f"{method} can only be called with a valid solution or in stage SOLVING (current stage: {SCIPgetStage(self.scip)})")

def getOrigin(self):
"""
Returns origin of solution: where to retrieve uncached elements.
Returns
-------
PY_SCIP_SOLORIGIN
"""
return SCIPsolGetOrigin(self.sol)

def retransform(self):
""" retransforms solution to original problem space """
PY_SCIP_CALL(SCIPretransformSol(self.scip, self.sol))

def translate(self, Model target):
"""
translate solution to a target model solution
Parameters
----------
target : Model
Returns
-------
targetSol: Solution
"""
if self.getOrigin() != SCIP_SOLORIGIN_ORIGINAL:
PY_SCIP_CALL(SCIPretransformSol(self.scip, self.sol))
cdef Solution targetSol = Solution.create(target._scip, NULL)
cdef SCIP_VAR** source_vars = SCIPgetOrigVars(self.scip)

PY_SCIP_CALL(SCIPtranslateSubSol(target._scip, self.scip, self.sol, NULL, source_vars, &(targetSol.sol)))
return targetSol


cdef class BoundChange:
"""Bound change."""
Expand Down Expand Up @@ -6170,6 +6214,14 @@ cdef class Model:
"""Optimize the problem."""
PY_SCIP_CALL(SCIPsolve(self._scip))
self._bestSol = Solution.create(self._scip, SCIPgetBestSol(self._scip))

def optimizeNogil(self):
"""Optimize the problem without GIL."""
cdef SCIP_RETCODE rc;
with nogil:
rc = SCIPsolve(self._scip)
PY_SCIP_CALL(rc)
self._bestSol = Solution.create(self._scip, SCIPgetBestSol(self._scip))

def solveConcurrent(self):
"""Transforms, presolves, and solves problem using additional solvers which emphasize on
Expand Down
2 changes: 2 additions & 0 deletions tests/test_copy.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from pyscipopt import Model
from helpers.utils import random_mip_1

def test_copy():
# create solver instance
Expand All @@ -18,3 +19,4 @@ def test_copy():
s2.optimize()

assert s.getObjVal() == s2.getObjVal()

23 changes: 23 additions & 0 deletions tests/test_nogil.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from concurrent.futures import ThreadPoolExecutor, as_completed
from pyscipopt import Model
from helpers.utils import random_mip_1

N_Threads = 4


def test_optimalNogil():
ori_model = random_mip_1(disable_sepa=False, disable_huer=False, disable_presolve=False, node_lim=2000, small=True)
models = [Model(sourceModel=ori_model) for _ in range(N_Threads)]
for i in range(N_Threads):
models[i].setParam("randomization/permutationseed", i)

ori_model.optimize()

with ThreadPoolExecutor(max_workers=N_Threads) as executor:
futures = [executor.submit(Model.optimizeNogil, model) for model in models]
for future in as_completed(futures):
pass
for model in models:
assert model.getStatus() == "optimal"
assert abs(ori_model.getObjVal() - model.getObjVal()) < 1e-6

28 changes: 27 additions & 1 deletion tests/test_solution.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import re
import pytest
from pyscipopt import Model, scip, SCIP_PARAMSETTING, quicksum, quickprod
from pyscipopt import Model, scip, SCIP_PARAMSETTING, quicksum, quickprod, SCIP_SOLORIGIN
from helpers.utils import random_mip_1


def test_solution_getbest():
Expand Down Expand Up @@ -193,3 +194,28 @@ def test_getSols():

assert len(m.getSols()) >= 1
assert any(m.isEQ(sol[x], 0.0) for sol in m.getSols())


def test_getOrigin_retrasform():
m = random_mip_1(disable_sepa=False, disable_huer=False, disable_presolve=False, small=True)
m.optimize()

sol = m.getBestSol()
assert sol.getOrigin() == SCIP_SOLORIGIN.ZERO

sol.retransform()
assert sol.getOrigin() == SCIP_SOLORIGIN.ORIGINAL


def test_translate():
m = random_mip_1(disable_sepa=False, disable_huer=False, disable_presolve=False, small=True)
m.optimize()
sol = m.getBestSol()

m1 = random_mip_1(disable_sepa=False, disable_huer=False, disable_presolve=False, small=True)
sol1 = sol.translate(m1)
assert m1.addSol(sol1) == True
assert m1.getNSols() == 1
m1.optimize()
assert m.getObjVal() == m1.getObjVal()

0 comments on commit 6876af9

Please sign in to comment.