diff --git a/check/TestLpSolvers.cpp b/check/TestLpSolvers.cpp index 90fdcd4142..41cb925526 100644 --- a/check/TestLpSolvers.cpp +++ b/check/TestLpSolvers.cpp @@ -553,3 +553,32 @@ TEST_CASE("choose-lp-solver", "[highs_lp_solver]") { h.resetGlobalScheduler(true); } + +TEST_CASE("releaseMemory", "[highs_lp_solver]") { + std::string model_file = + std::string(HIGHS_DIR) + "/check/instances/avgas.mps"; + Highs h; + h.setOptionValue("output_flag", dev_run); + + // First solve + REQUIRE(h.readModel(model_file) == HighsStatus::kOk); + REQUIRE(h.run() == HighsStatus::kOk); + REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); + double first_objective = h.getInfo().objective_function_value; + + // Release memory and verify we can solve again + REQUIRE(h.releaseMemory() == HighsStatus::kOk); + + // Second solve on a different model + std::string model_file2 = + std::string(HIGHS_DIR) + "/check/instances/adlittle.mps"; + REQUIRE(h.readModel(model_file2) == HighsStatus::kOk); + REQUIRE(h.run() == HighsStatus::kOk); + REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); + double second_objective = h.getInfo().objective_function_value; + + // Verify objectives are different (different problems) + REQUIRE(first_objective != second_objective); + + h.resetGlobalScheduler(true); +} diff --git a/highs/Highs.h b/highs/Highs.h index 35b9a3cb7f..05ebce2ffb 100644 --- a/highs/Highs.h +++ b/highs/Highs.h @@ -89,6 +89,16 @@ class Highs { */ HighsStatus clearSolverDualData(); + /** + * @brief Release all retained memory back to the allocator + * + * Clears all solver state and shrinks internal vectors to free + * unused capacity. Useful in long-running services that reuse a + * Highs instance across multiple solves to prevent unbounded RSS + * growth from heap fragmentation. + */ + HighsStatus releaseMemory(); + /** * Methods for model input */ diff --git a/highs/highs_bindings.cpp b/highs/highs_bindings.cpp index 42dd112caf..b222ca457c 100644 --- a/highs/highs_bindings.cpp +++ b/highs/highs_bindings.cpp @@ -1332,6 +1332,7 @@ PYBIND11_MODULE(_core, m, py::mod_gil_not_used()) { .def("clear", &Highs::clear) .def("clearModel", &Highs::clearModel) .def("clearSolver", &Highs::clearSolver) + .def("releaseMemory", &Highs::releaseMemory) .def("passModel", &highs_passModel) .def("passModel", &highs_passModelPointers) .def("passModel", &highs_passLp) diff --git a/highs/highspy/_core/__init__.pyi b/highs/highspy/_core/__init__.pyi index 05d231293d..6d9a7658ac 100644 --- a/highs/highspy/_core/__init__.pyi +++ b/highs/highspy/_core/__init__.pyi @@ -955,6 +955,7 @@ class _Highs: def clearModel(self) -> HighsStatus: ... def clearLinearObjectives(self) -> HighsStatus: ... def clearSolver(self) -> HighsStatus: ... + def releaseMemory(self) -> HighsStatus: ... def crossover(self, solution: HighsSolution) -> HighsStatus: ... def deleteCols(self, num_cols: int, cols: HighsIntArrayType) -> HighsStatus: ... def deleteRows(self, num_rows: int, rows: HighsIntArrayType) -> HighsStatus: ... diff --git a/highs/interfaces/highs_c_api.cpp b/highs/interfaces/highs_c_api.cpp index bc5cd03991..f7d52b84d2 100644 --- a/highs/interfaces/highs_c_api.cpp +++ b/highs/interfaces/highs_c_api.cpp @@ -379,6 +379,10 @@ HighsInt Highs_clearSolver(void* highs) { return (HighsInt)((Highs*)highs)->clearSolver(); } +HighsInt Highs_releaseMemory(void* highs) { + return (HighsInt)((Highs*)highs)->releaseMemory(); +} + HighsInt Highs_setBoolOptionValue(void* highs, const char* option, const HighsInt value) { return (HighsInt)((Highs*)highs) diff --git a/highs/interfaces/highs_c_api.h b/highs/interfaces/highs_c_api.h index a45c04bbb4..bca71d1eb8 100644 --- a/highs/interfaces/highs_c_api.h +++ b/highs/interfaces/highs_c_api.h @@ -395,6 +395,20 @@ HighsInt Highs_clearModel(void* highs); */ HighsInt Highs_clearSolver(void* highs); +/** + * Release all retained memory back to the allocator. + * + * Clears all solver state and shrinks internal vectors to free unused + * capacity. Useful in long-running services that reuse a Highs instance + * across multiple solves to prevent unbounded RSS growth from heap + * fragmentation. + * + * @param highs A pointer to the Highs instance. + * + * @returns A `kHighsStatus` constant indicating whether the call succeeded. + */ +HighsInt Highs_releaseMemory(void* highs); + /** * Presolve a model. * diff --git a/highs/lp_data/Highs.cpp b/highs/lp_data/Highs.cpp index 92f568dd79..5753e9fc55 100644 --- a/highs/lp_data/Highs.cpp +++ b/highs/lp_data/Highs.cpp @@ -58,6 +58,7 @@ HighsStatus Highs::clear() { HighsStatus Highs::clearModel() { model_.clear(); multi_linear_objective_.clear(); + saved_objective_and_solution_.clear(); return clearSolver(); } @@ -65,6 +66,7 @@ HighsStatus Highs::clearSolver() { HighsStatus return_status = HighsStatus::kOk; clearDerivedModelProperties(); invalidateSolverData(); + ekk_instance_.clear(); return returnFromHighs(return_status); } @@ -75,6 +77,61 @@ HighsStatus Highs::clearSolverDualData() { return returnFromHighs(return_status); } +HighsStatus Highs::releaseMemory() { + HighsStatus return_status = HighsStatus::kOk; + // 1. Clear all model and solver state (same as clear()). + clearModel(); + + // 2. Clear and shrink vectors that clearModel/clearSolver only + // invalidate or leave with residual capacity. After this block the + // instance holds zero allocated heap memory for solver state — + // equivalent to a freshly constructed Highs. + saved_objective_and_solution_.shrink_to_fit(); + solution_.clear(); + solution_.col_value.shrink_to_fit(); + solution_.col_dual.shrink_to_fit(); + solution_.row_value.shrink_to_fit(); + solution_.row_dual.shrink_to_fit(); + basis_.clear(); + basis_.col_status.shrink_to_fit(); + basis_.row_status.shrink_to_fit(); + ranging_.clear(); + ranging_.col_cost_up.value_.shrink_to_fit(); + ranging_.col_cost_up.objective_.shrink_to_fit(); + ranging_.col_cost_up.in_var_.shrink_to_fit(); + ranging_.col_cost_up.ou_var_.shrink_to_fit(); + ranging_.col_cost_dn.value_.shrink_to_fit(); + ranging_.col_cost_dn.objective_.shrink_to_fit(); + ranging_.col_cost_dn.in_var_.shrink_to_fit(); + ranging_.col_cost_dn.ou_var_.shrink_to_fit(); + ranging_.col_bound_up.value_.shrink_to_fit(); + ranging_.col_bound_up.objective_.shrink_to_fit(); + ranging_.col_bound_up.in_var_.shrink_to_fit(); + ranging_.col_bound_up.ou_var_.shrink_to_fit(); + ranging_.col_bound_dn.value_.shrink_to_fit(); + ranging_.col_bound_dn.objective_.shrink_to_fit(); + ranging_.col_bound_dn.in_var_.shrink_to_fit(); + ranging_.col_bound_dn.ou_var_.shrink_to_fit(); + ranging_.row_bound_up.value_.shrink_to_fit(); + ranging_.row_bound_up.objective_.shrink_to_fit(); + ranging_.row_bound_up.in_var_.shrink_to_fit(); + ranging_.row_bound_up.ou_var_.shrink_to_fit(); + ranging_.row_bound_dn.value_.shrink_to_fit(); + ranging_.row_bound_dn.objective_.shrink_to_fit(); + ranging_.row_bound_dn.in_var_.shrink_to_fit(); + ranging_.row_bound_dn.ou_var_.shrink_to_fit(); + iis_.clear(); + + // 3. Shrink standard form LP vectors. + standard_form_cost_.shrink_to_fit(); + standard_form_rhs_.shrink_to_fit(); + + // 4. Reset timer clocks (accumulated timing data). + timer_.zeroAllClocks(); + + return returnFromHighs(return_status); +} + HighsStatus Highs::setOptionValue(const std::string& option, const bool value) { if (setLocalOptionValue(options_.log_options, option, options_.records, value) == OptionStatus::kOk) diff --git a/tests/test_highspy.py b/tests/test_highspy.py index fae4c286e6..445c440330 100644 --- a/tests/test_highspy.py +++ b/tests/test_highspy.py @@ -1514,6 +1514,24 @@ def try_change_ptr(e): h.solve() self.assertEqual(check_called[0], True) + def test_releaseMemory(self): + """Test that releaseMemory() frees memory and allows solving again.""" + # Create and solve first problem + h = self.get_example_model() + h.run() + first_objective = h.getInfo().objective_function_value + + # Release memory + status = h.releaseMemory() + self.assertEqual(status, highspy.HighsStatus.kOk) + + # Solve a different problem to verify the solver still works + h2 = self.get_basic_model() + h2.run() + second_objective = h2.getInfo().objective_function_value + + # Verify objectives are different (different problems) + self.assertNotEqual(first_objective, second_objective) def test_addVars_invalid_parameter(self): """ensure_real raises on invalid parameter""" h = highspy.Highs()