Skip to content

Commit 63af694

Browse files
authored
Merge pull request #3095 from ERGO-Code/pr-2988-latest
PR 2988 with latest merged in
2 parents 15ccb65 + 709b30e commit 63af694

8 files changed

Lines changed: 134 additions & 0 deletions

File tree

check/TestLpSolvers.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,3 +553,32 @@ TEST_CASE("choose-lp-solver", "[highs_lp_solver]") {
553553

554554
h.resetGlobalScheduler(true);
555555
}
556+
557+
TEST_CASE("releaseMemory", "[highs_lp_solver]") {
558+
std::string model_file =
559+
std::string(HIGHS_DIR) + "/check/instances/avgas.mps";
560+
Highs h;
561+
h.setOptionValue("output_flag", dev_run);
562+
563+
// First solve
564+
REQUIRE(h.readModel(model_file) == HighsStatus::kOk);
565+
REQUIRE(h.run() == HighsStatus::kOk);
566+
REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal);
567+
double first_objective = h.getInfo().objective_function_value;
568+
569+
// Release memory and verify we can solve again
570+
REQUIRE(h.releaseMemory() == HighsStatus::kOk);
571+
572+
// Second solve on a different model
573+
std::string model_file2 =
574+
std::string(HIGHS_DIR) + "/check/instances/adlittle.mps";
575+
REQUIRE(h.readModel(model_file2) == HighsStatus::kOk);
576+
REQUIRE(h.run() == HighsStatus::kOk);
577+
REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal);
578+
double second_objective = h.getInfo().objective_function_value;
579+
580+
// Verify objectives are different (different problems)
581+
REQUIRE(first_objective != second_objective);
582+
583+
h.resetGlobalScheduler(true);
584+
}

highs/Highs.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,16 @@ class Highs {
9595
*/
9696
HighsStatus clearSolverDualData();
9797

98+
/**
99+
* @brief Release all retained memory back to the allocator
100+
*
101+
* Clears all solver state and shrinks internal vectors to free
102+
* unused capacity. Useful in long-running services that reuse a
103+
* Highs instance across multiple solves to prevent unbounded RSS
104+
* growth from heap fragmentation.
105+
*/
106+
HighsStatus releaseMemory();
107+
98108
/**
99109
* Methods for model input
100110
*/

highs/interfaces/highs_c_api.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,10 @@ HighsInt Highs_clearSolver(void* highs) {
379379
return (HighsInt)((Highs*)highs)->clearSolver();
380380
}
381381

382+
HighsInt Highs_releaseMemory(void* highs) {
383+
return (HighsInt)((Highs*)highs)->releaseMemory();
384+
}
385+
382386
HighsInt Highs_setBoolOptionValue(void* highs, const char* option,
383387
const HighsInt value) {
384388
return (HighsInt)((Highs*)highs)

highs/interfaces/highs_c_api.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,20 @@ HighsInt Highs_clearModel(void* highs);
395395
*/
396396
HighsInt Highs_clearSolver(void* highs);
397397

398+
/**
399+
* Release all retained memory back to the allocator.
400+
*
401+
* Clears all solver state and shrinks internal vectors to free unused
402+
* capacity. Useful in long-running services that reuse a Highs instance
403+
* across multiple solves to prevent unbounded RSS growth from heap
404+
* fragmentation.
405+
*
406+
* @param highs A pointer to the Highs instance.
407+
*
408+
* @returns A `kHighsStatus` constant indicating whether the call succeeded.
409+
*/
410+
HighsInt Highs_releaseMemory(void* highs);
411+
398412
/**
399413
* Presolve a model.
400414
*

highs/lp_data/Highs.cpp

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,15 @@ HighsStatus Highs::clear() {
6363
HighsStatus Highs::clearModel() {
6464
model_.clear();
6565
multi_linear_objective_.clear();
66+
saved_objective_and_solution_.clear();
6667
return clearSolver();
6768
}
6869

6970
HighsStatus Highs::clearSolver() {
7071
HighsStatus return_status = HighsStatus::kOk;
7172
clearDerivedModelProperties();
7273
invalidateSolverData();
74+
ekk_instance_.clear();
7375
return returnFromHighs(return_status);
7476
}
7577

@@ -80,6 +82,61 @@ HighsStatus Highs::clearSolverDualData() {
8082
return returnFromHighs(return_status);
8183
}
8284

85+
HighsStatus Highs::releaseMemory() {
86+
HighsStatus return_status = HighsStatus::kOk;
87+
// 1. Clear all model and solver state (same as clear()).
88+
clearModel();
89+
90+
// 2. Clear and shrink vectors that clearModel/clearSolver only
91+
// invalidate or leave with residual capacity. After this block the
92+
// instance holds zero allocated heap memory for solver state —
93+
// equivalent to a freshly constructed Highs.
94+
saved_objective_and_solution_.shrink_to_fit();
95+
solution_.clear();
96+
solution_.col_value.shrink_to_fit();
97+
solution_.col_dual.shrink_to_fit();
98+
solution_.row_value.shrink_to_fit();
99+
solution_.row_dual.shrink_to_fit();
100+
basis_.clear();
101+
basis_.col_status.shrink_to_fit();
102+
basis_.row_status.shrink_to_fit();
103+
ranging_.clear();
104+
ranging_.col_cost_up.value_.shrink_to_fit();
105+
ranging_.col_cost_up.objective_.shrink_to_fit();
106+
ranging_.col_cost_up.in_var_.shrink_to_fit();
107+
ranging_.col_cost_up.ou_var_.shrink_to_fit();
108+
ranging_.col_cost_dn.value_.shrink_to_fit();
109+
ranging_.col_cost_dn.objective_.shrink_to_fit();
110+
ranging_.col_cost_dn.in_var_.shrink_to_fit();
111+
ranging_.col_cost_dn.ou_var_.shrink_to_fit();
112+
ranging_.col_bound_up.value_.shrink_to_fit();
113+
ranging_.col_bound_up.objective_.shrink_to_fit();
114+
ranging_.col_bound_up.in_var_.shrink_to_fit();
115+
ranging_.col_bound_up.ou_var_.shrink_to_fit();
116+
ranging_.col_bound_dn.value_.shrink_to_fit();
117+
ranging_.col_bound_dn.objective_.shrink_to_fit();
118+
ranging_.col_bound_dn.in_var_.shrink_to_fit();
119+
ranging_.col_bound_dn.ou_var_.shrink_to_fit();
120+
ranging_.row_bound_up.value_.shrink_to_fit();
121+
ranging_.row_bound_up.objective_.shrink_to_fit();
122+
ranging_.row_bound_up.in_var_.shrink_to_fit();
123+
ranging_.row_bound_up.ou_var_.shrink_to_fit();
124+
ranging_.row_bound_dn.value_.shrink_to_fit();
125+
ranging_.row_bound_dn.objective_.shrink_to_fit();
126+
ranging_.row_bound_dn.in_var_.shrink_to_fit();
127+
ranging_.row_bound_dn.ou_var_.shrink_to_fit();
128+
iis_.clear();
129+
130+
// 3. Shrink standard form LP vectors.
131+
standard_form_cost_.shrink_to_fit();
132+
standard_form_rhs_.shrink_to_fit();
133+
134+
// 4. Reset timer clocks (accumulated timing data).
135+
timer_.zeroAllClocks();
136+
137+
return returnFromHighs(return_status);
138+
}
139+
83140
HighsStatus Highs::setOptionValue(const std::string& option, const bool value) {
84141
if (setLocalOptionValue(options_.log_options, option, options_.records,
85142
value) == OptionStatus::kOk)

highspy/highs_bindings.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1380,6 +1380,7 @@ PYBIND11_MODULE(_core, m, py::mod_gil_not_used()) {
13801380
.def("clear", &Highs::clear)
13811381
.def("clearModel", &Highs::clearModel)
13821382
.def("clearSolver", &Highs::clearSolver)
1383+
.def("releaseMemory", &Highs::releaseMemory)
13831384
.def("passModel", &highs_passModel)
13841385
.def("passModel", &highs_passModelPointers)
13851386
.def("passModel", &highs_passLp)

highspy/highspy/_core/__init__.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,7 @@ class _Highs:
955955
def clearModel(self) -> HighsStatus: ...
956956
def clearLinearObjectives(self) -> HighsStatus: ...
957957
def clearSolver(self) -> HighsStatus: ...
958+
def releaseMemory(self) -> HighsStatus: ...
958959
def crossover(self, solution: HighsSolution) -> HighsStatus: ...
959960
def deleteCols(self, num_cols: int, cols: HighsIntArrayType) -> HighsStatus: ...
960961
def deleteRows(self, num_rows: int, rows: HighsIntArrayType) -> HighsStatus: ...

highspy/tests/test_highspy.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1547,6 +1547,24 @@ def try_change_ptr(e):
15471547
h.solve()
15481548
self.assertEqual(check_called[0], True)
15491549

1550+
def test_releaseMemory(self):
1551+
"""Test that releaseMemory() frees memory and allows solving again."""
1552+
# Create and solve first problem
1553+
h = self.get_example_model()
1554+
h.run()
1555+
first_objective = h.getInfo().objective_function_value
1556+
1557+
# Release memory
1558+
status = h.releaseMemory()
1559+
self.assertEqual(status, highspy.HighsStatus.kOk)
1560+
1561+
# Solve a different problem to verify the solver still works
1562+
h2 = self.get_basic_model()
1563+
h2.run()
1564+
second_objective = h2.getInfo().objective_function_value
1565+
1566+
# Verify objectives are different (different problems)
1567+
self.assertNotEqual(first_objective, second_objective)
15501568
def test_addVars_invalid_parameter(self):
15511569
"""ensure_real raises on invalid parameter"""
15521570
h = highspy.Highs()

0 commit comments

Comments
 (0)