diff --git a/ci/test_self_hosted_service.sh b/ci/test_self_hosted_service.sh index 81126c836d..a5285ec1ca 100755 --- a/ci/test_self_hosted_service.sh +++ b/ci/test_self_hosted_service.sh @@ -217,6 +217,10 @@ if [ "$doservertest" -eq 1 ]; then # Test for message on absolute path, bad directory run_cli_test "Absolute path '/nohay' does not exist" cuopt_sh -s -c "$CLIENT_CERT" -p $CUOPT_SERVER_PORT -f /nohay/nada + # Set all current and deprecated solver_config values and make sure the service does not reject the dataset + # This is a smoketest against parameter name misalignment + run_cli_test "'status': 'Optimal'" cuopt_sh -s -c $CLIENT_CERT -p $CUOPT_SERVER_PORT ../../datasets/cuopt_service_data/lpmip_configs.json + rapids-logger "Running cuopt_self_hosted Python tests" pytest tests diff --git a/datasets/cuopt_service_data/lpmip_configs.json b/datasets/cuopt_service_data/lpmip_configs.json new file mode 100644 index 0000000000..cb920de508 --- /dev/null +++ b/datasets/cuopt_service_data/lpmip_configs.json @@ -0,0 +1,103 @@ +{ + "csr_constraint_matrix": { + "offsets": [ + 0, + 2, + 4 + ], + "indices": [ + 0, + 1, + 0, + 1 + ], + "values": [ + 3.0, + 4.0, + 2.7, + 10.1 + ] + }, + "constraint_bounds": { + "bounds": [ + 5.4, + 4.9 + ], + "upper_bounds": [ + 5.4, + 4.9 + ], + "lower_bounds": [ + "ninf", + "ninf" + ] + }, + "objective_data": { + "coefficients": [ + 0.2, + 0.1 + ], + "scalability_factor": 1.0, + "offset": 0.0 + }, + "variable_bounds": { + "upper_bounds": [ + "inf", + "inf" + ], + "lower_bounds": [ + 0.0, + 0.0 + ] + }, + "maximize": false, + "solver_config": { + "tolerances": { + "optimality": 0.0001, + "absolute_primal_tolerance": 0.0001, + "absolute_dual_tolerance": 0.0001, + "absolute_gap_tolerance": 0.0001, + "relative_primal_tolerance": 0.0001, + "relative_dual_tolerance": 0.0001, + "relative_gap_tolerance": 0.0001, + "primal_infeasible_tolerance": 0.0001, + "dual_infeasible_tolerance": 0.0001, + "mip_integrality_tolerance": 0.0001, + "mip_absolute_gap": 0.0001, + "mip_relative_gap": 0.0001, + "mip_absolute_tolerance": 0.0001, + "mip_relative_tolerance": 0.0001, + + "absolute_primal": 0.0001, + "absolute_dual": 0.0001, + "absolute_gap": 0.0001, + "relative_primal": 0.0001, + "relative_dual": 0.0001, + "relative_gap": 0.0001, + "primal_infeasible": 0.0001, + "dual_infeasible": 0.0001, + "integrality_tolerance": 0.0001, + "absolute_mip_gap": 0.0001, + "relative_mip_gap": 0.0001 + }, + "infeasibility_detection": true, + "time_limit": 5, + "iteration_limit": 100, + "pdlp_solver_mode": 2, + "method": 2, + "mip_scaling": true, + "mip_heuristics_only": true, + "num_cpu_threads": 100, + "crossover": true, + "log_to_console": false, + "strict_infeasibility": false, + "user_problem_file": "bob", + "per_constraint_residual": true, + "save_best_primal_so_far": true, + "first_primal_feasible": true, + "log_file": "bill", + "solution_file": "barry", + "solver_mode": 3, + "heuristics_only": false + } +} diff --git a/docs/cuopt/source/lp-features.rst b/docs/cuopt/source/lp-features.rst index f3861ffacd..e89ee8f00b 100644 --- a/docs/cuopt/source/lp-features.rst +++ b/docs/cuopt/source/lp-features.rst @@ -76,9 +76,17 @@ Crossover Crossover allows you to obtain a high-quality basic solution from the results of a PDLP solve. More details can be found :ref:`here `. -Logging Callback ----------------- -With logging callback, users can fetch server-side logs for additional debugs and to get details on solver process details. :ref:`Examples ` are shared on the self-hosted page. +Logging +------- + +The CUOPT_LOG_FILE parameter can be set to write detailed solver logs for LP problems. This parameter is available in all APIs that allow setting solver parameters except the cuOpt service. For the service, see the logging callback below. + +Logging Callback in the Service +------------------------------- + +In the cuOpt service API, the ``log_file`` value in ``solver_configs`` is ignored. + +If however you set the ``solver_logs`` flag on the ``/cuopt/request`` REST API call, users can fetch the log file content from the webserver at ``/cuopt/logs/{id}``. Using the logging callback feature through the cuOpt client is shown in :ref:`Examples ` on the self-hosted page. Infeasibility Detection diff --git a/docs/cuopt/source/lp-milp-settings.rst b/docs/cuopt/source/lp-milp-settings.rst index 28e5105d09..8e15f36c79 100644 --- a/docs/cuopt/source/lp-milp-settings.rst +++ b/docs/cuopt/source/lp-milp-settings.rst @@ -39,19 +39,19 @@ Log File ^^^^^^^^ ``CUOPT_LOG_FILE`` controls the name of a log file where cuOpt should write information about the solve. -Note: the default value is ``""`` and no log file is written. +Note: the default value is ``""`` and no log file is written. This setting is ignored by the cuOpt service, use the log callback feature instead. Solution File ^^^^^^^^^^^^^ -``CUOPT_SOL_FILE`` controls the name of a file where cuOpt should write the solution. +``CUOPT_SOLUTION_FILE`` controls the name of a file where cuOpt should write the solution. -Note: the default value is ``""`` and no solution file is written. +Note: the default value is ``""`` and no solution file is written. This setting is ignored by the cuOpt service. User Problem File ^^^^^^^^^^^^^^^^^ ``CUOPT_USER_PROBLEM_FILE`` controls the name of a file where cuOpt should write the user problem. -Note: the default value is ``""`` and no user problem file is written. +Note: the default value is ``""`` and no user problem file is written. This setting is ignored by the cuOpt service. Num CPU Threads ^^^^^^^^^^^^^^^ @@ -257,7 +257,7 @@ We now describe parameter settings for the MILP solvers Heuristics only ^^^^^^^^^^^^^^^ -``CUOPT_HEURISTICS_ONLY`` controls if only the GPU heuristics should be run. When set to true, only the primal +``CUOPT_MIP_HEURISTICS_ONLY`` controls if only the GPU heuristics should be run for the MIP problem. When set to true, only the primal bound is improved via the GPU. When set to false, both the GPU and CPU are used and the dual bound is improved on the CPU. @@ -275,14 +275,14 @@ Note: the defaulte value is true. Absolute Tolerance ^^^^^^^^^^^^^^^^^^ -``CUOPT_ABSOLUTE_TOLERANCE`` controls the MIP absolute tolerance. +``CUOPT_MIP_ABSOLUTE_TOLERANCE`` controls the MIP absolute tolerance. Note: the default value is ``1e-4``. Relative Tolerance ^^^^^^^^^^^^^^^^^^ -``CUOPT_RELATIVE_TOLERANCE`` controls the MIP relative tolerance. +``CUOPT_MIP_RELATIVE_TOLERANCE`` controls the MIP relative tolerance. Note: the default value is ``1e-6``. diff --git a/docs/cuopt/source/milp-features.rst b/docs/cuopt/source/milp-features.rst index 814207a1c0..3891561084 100644 --- a/docs/cuopt/source/milp-features.rst +++ b/docs/cuopt/source/milp-features.rst @@ -50,15 +50,23 @@ There are two ways to specify constraints in cuOpt MILP: Both forms are mathematically equivalent. The choice between them is a matter of convenience depending on your problem formulation. -Incumbent Solution Callback ---------------------------- +Incumbent Solution Callback in the Service +------------------------------------------ -User can provide a callback to receive new integer feasible solutions that improve the objective (called incumbents) while the solver is running. An :ref:`Incumbent Example ` is shared on the self-hosted page. +When using the service, users can provide a callback to receive new integer feasible solutions that improve the objective (called incumbents) while the solver is running. An :ref:`Incumbent Example ` is shared on the self-hosted page. -Logging Callback ----------------- +Logging +------- + +The CUOPT_LOG_FILE parameter can be set to write detailed solver logs for MILP problems. This parameter is available in all APIs that allow setting solver parameters except for the cuOpt service. For the service, see the logging callback below. + +Logging Callback in the Service +------------------------------- + +In the cuOpt service API, the ``log_file`` value in ``solver_configs`` is ignored. + +If however you set the ``solver_logs`` flag on the ``/cuopt/request`` REST API call, users can fetch the log file content from the webserver at ``/cuopt/logs/{id}``. Using the logging callback feature through the cuOpt client is shown in :ref:`Logging Callback Example ` on the self-hosted page. -A logging callback allows users to get additional information about how the solve is progressing. A :ref:`Logging Callback Example ` is shared on the self-hosted page. Time Limit -------------- diff --git a/docs/cuopt/source/transition.rst b/docs/cuopt/source/transition.rst index 97d48f7b71..dd3d47bbf1 100644 --- a/docs/cuopt/source/transition.rst +++ b/docs/cuopt/source/transition.rst @@ -1,6 +1,6 @@ -======================================== +======================================= Transition Guide for Change in Features -======================================== +======================================= In addition to the quality improvements, some new features were added, and some features were deprecated to improve user experience. For any questions, please reach out to the cuOpt team through github issues. @@ -10,8 +10,72 @@ Parameter/option statuses are listed below, they express how each of these optio **Update** - A change in definition of feature. - **Deprecated** - These are “no operation” options, they will be accepted by the server, but they will not be used anywhere. And the solver will also return a warning about them being deprecated. + **Deprecated** - These options will be accepted but will be removed in the future. In the case of the cuOpt service, the server will also return a warning noting that a feature is deprecated. **Limited** - These options are limited with respect to the number of dimensions that can be provided. - **Removed** - These features were deprecated in previous release and completely removed in this one. \ No newline at end of file + **Removed** - These features were deprecated in a previous release and completely removed in this one. + +For all solver_configs fields, see the LP/MILP settings guide :doc:`lp-milp-settings` or the service openapi spec :doc:`open-api`. + +Changes to solver_configs.tolerances +------------------------------------ + +The following fields are **Deprecated** in ``solver_configs.tolerances`` for the service: + +- absolute_primal +- absolute_dual +- absolute_gap +- relative_primal +- relative_dual +- relative_gap +- primal_infeasible +- dual_infeasible +- integrality_tolerance +- absolute_mip_gap +- relative_mip_gap + +The following fields are **New** in ``solver_configs.tolerances`` for the service and replace the deprecated fields above: + +- absolute_primal_tolerance +- absolute_dual_tolerance +- absolute_gap_tolerance +- relative_primal_tolerance +- relative_dual_tolerance +- relative_gap_tolerance +- primal_infeasible_tolerance +- dual_infeasible_tolerance +- mip_integrality_tolerance +- mip_absolute_gap +- mip_relative_gap + +The following fields are **New** in ``solver_configs.tolerances`` for the service but were available in the C API in 25.05 + +- mip_absolute_tolerance +- mip_relative_tolerance + +Changes to solver_configs +------------------------- + +The following fields are **Deprecated** in ``solver_configs`` for the service: + +- solver_mode +- heuristics_only + +The following fields are **New** in ``solver_configs`` for the service and replace the deprecated fields above: + +- pdlp_solver_mode +- mip_heuristics_only + +The following are **New** in ``solver_configs`` for the service but were available in the C API in 25.05 + +- strict_infeasibility +- user_problem_file +- per_constraint_residual +- save_best_primal_so_far +- first_primal_feasible +- log_file +- solution_file + + + diff --git a/python/cuopt/cuopt/linear_programming/solver/solver.py b/python/cuopt/cuopt/linear_programming/solver/solver.py index 24812e70c9..4543cd523f 100644 --- a/python/cuopt/cuopt/linear_programming/solver/solver.py +++ b/python/cuopt/cuopt/linear_programming/solver/solver.py @@ -19,7 +19,7 @@ @catch_cuopt_exception -def Solve(data_model, solver_settings=None, log_file=""): +def Solve(data_model, solver_settings=None): """ Solve the Linear Program passed as input and returns the solution. @@ -92,13 +92,12 @@ def is_mip(var_types): return solver_wrapper.Solve( data_model, solver_settings, - log_file, mip=is_mip(data_model.get_variable_types()), ) @catch_cuopt_exception -def BatchSolve(data_model_list, solver_settings=None, log_file=""): +def BatchSolve(data_model_list, solver_settings=None): """ Solve the list of Linear Programs passed as input and returns the solutions and total solve time. @@ -174,6 +173,4 @@ def BatchSolve(data_model_list, solver_settings=None, log_file=""): if solver_settings is None: solver_settings = SolverSettings() - return solver_wrapper.BatchSolve( - data_model_list, solver_settings, log_file - ) + return solver_wrapper.BatchSolve(data_model_list, solver_settings) diff --git a/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx b/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx index 93a303489b..02782b8f9b 100644 --- a/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx @@ -65,7 +65,6 @@ from numba import cuda import cudf from cudf.core.buffer import as_buffer -from cuopt.linear_programming.solver.solver_parameters import CUOPT_LOG_FILE from cuopt.linear_programming.solver_settings.solver_settings import ( PDLPSolverMode, SolverSettings, @@ -279,7 +278,6 @@ cdef set_data_model_view(DataModel data_model_obj): cdef set_solver_setting( unique_ptr[solver_settings_t[int, double]]& unique_solver_settings, settings, - log_file, DataModel data_model_obj=None, mip=False): cdef solver_settings_t[int, double]* c_solver_settings = ( @@ -425,13 +423,6 @@ cdef set_solver_setting( settings.get_pdlp_warm_start_data().iterations_since_last_restart # noqa ) - # Common to LP and MIP - - c_solver_settings.set_parameter_from_string( - CUOPT_LOG_FILE.encode('utf-8'), - log_file.encode('utf-8') - ) - cdef create_solution(unique_ptr[solver_ret_t] sol_ret_ptr, DataModel data_model_obj, is_batch=False): @@ -670,7 +661,7 @@ cdef create_solution(unique_ptr[solver_ret_t] sol_ret_ptr, ) -def Solve(py_data_model_obj, settings, str log_file, mip=False): +def Solve(py_data_model_obj, settings, mip=False): cdef DataModel data_model_obj = py_data_model_obj cdef unique_ptr[solver_settings_t[int, double]] unique_solver_settings @@ -682,7 +673,7 @@ def Solve(py_data_model_obj, settings, str log_file, mip=False): ) set_solver_setting( - unique_solver_settings, settings, log_file, data_model_obj, mip + unique_solver_settings, settings, data_model_obj, mip ) set_data_model_view(data_model_obj) @@ -697,13 +688,13 @@ cdef insert_vector(DataModel data_model_obj, data_model_views.push_back(data_model_obj.c_data_model_view.get()) -def BatchSolve(py_data_model_list, settings, str log_file): +def BatchSolve(py_data_model_list, settings): cdef unique_ptr[solver_settings_t[int, double]] unique_solver_settings unique_solver_settings.reset(new solver_settings_t[int, double]()) if settings.get_pdlp_warm_start_data() is not None: # noqa raise Exception("Cannot use warmstart data with Batch Solve") - set_solver_setting(unique_solver_settings, settings, log_file) + set_solver_setting(unique_solver_settings, settings) cdef vector[data_model_view_t[int, double] *] data_model_views diff --git a/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py b/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py index 9159ba9331..9f429e6550 100644 --- a/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py +++ b/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py @@ -21,22 +21,31 @@ CUOPT_ABSOLUTE_PRIMAL_TOLERANCE, CUOPT_CROSSOVER, CUOPT_DUAL_INFEASIBLE_TOLERANCE, + CUOPT_FIRST_PRIMAL_FEASIBLE, CUOPT_INFEASIBILITY_DETECTION, CUOPT_ITERATION_LIMIT, + CUOPT_LOG_FILE, CUOPT_LOG_TO_CONSOLE, CUOPT_METHOD, CUOPT_MIP_ABSOLUTE_GAP, + CUOPT_MIP_ABSOLUTE_TOLERANCE, CUOPT_MIP_HEURISTICS_ONLY, CUOPT_MIP_INTEGRALITY_TOLERANCE, CUOPT_MIP_RELATIVE_GAP, + CUOPT_MIP_RELATIVE_TOLERANCE, CUOPT_MIP_SCALING, CUOPT_NUM_CPU_THREADS, CUOPT_PDLP_SOLVER_MODE, + CUOPT_PER_CONSTRAINT_RESIDUAL, CUOPT_PRIMAL_INFEASIBLE_TOLERANCE, CUOPT_RELATIVE_DUAL_TOLERANCE, CUOPT_RELATIVE_GAP_TOLERANCE, CUOPT_RELATIVE_PRIMAL_TOLERANCE, + CUOPT_SAVE_BEST_PRIMAL_SO_FAR, + CUOPT_SOLUTION_FILE, + CUOPT_STRICT_INFEASIBILITY, CUOPT_TIME_LIMIT, + CUOPT_USER_PROBLEM_FILE, get_solver_setting, ) @@ -322,51 +331,72 @@ def toDict(self): time_limit = None solver_config = { - "tolerances": {}, + "tolerances": { + "absolute_dual_tolerance": self.get_parameter( + CUOPT_ABSOLUTE_DUAL_TOLERANCE + ), + "relative_dual_tolerance": self.get_parameter( + CUOPT_RELATIVE_DUAL_TOLERANCE + ), + "absolute_primal_tolerance": self.get_parameter( + CUOPT_ABSOLUTE_PRIMAL_TOLERANCE + ), + "relative_primal_tolerance": self.get_parameter( + CUOPT_RELATIVE_PRIMAL_TOLERANCE + ), + "absolute_gap_tolerance": self.get_parameter( + CUOPT_ABSOLUTE_GAP_TOLERANCE + ), + "relative_gap_tolerance": self.get_parameter( + CUOPT_RELATIVE_GAP_TOLERANCE + ), + "primal_infeasible_tolerance": self.get_parameter( + CUOPT_PRIMAL_INFEASIBLE_TOLERANCE + ), + "dual_infeasible_tolerance": self.get_parameter( + CUOPT_DUAL_INFEASIBLE_TOLERANCE + ), + "mip_integrality_tolerance": self.get_parameter( + CUOPT_MIP_INTEGRALITY_TOLERANCE + ), + "mip_absolute_gap": self.get_parameter(CUOPT_MIP_ABSOLUTE_GAP), + "mip_relative_gap": self.get_parameter(CUOPT_MIP_RELATIVE_GAP), + "mip_absolute_tolerance": self.get_parameter( + CUOPT_MIP_ABSOLUTE_TOLERANCE + ), + "mip_relative_tolerance": self.get_parameter( + CUOPT_MIP_RELATIVE_TOLERANCE + ), + }, "infeasibility_detection": self.get_parameter( CUOPT_INFEASIBILITY_DETECTION ), "time_limit": time_limit, "iteration_limit": self.get_parameter(CUOPT_ITERATION_LIMIT), - "solver_mode": self.get_parameter(CUOPT_PDLP_SOLVER_MODE), + "pdlp_solver_mode": self.get_parameter(CUOPT_PDLP_SOLVER_MODE), "method": self.get_parameter(CUOPT_METHOD), "mip_scaling": self.get_parameter(CUOPT_MIP_SCALING), - "heuristics_only": self.get_parameter(CUOPT_MIP_HEURISTICS_ONLY), + "mip_heuristics_only": self.get_parameter( + CUOPT_MIP_HEURISTICS_ONLY + ), "num_cpu_threads": self.get_parameter(CUOPT_NUM_CPU_THREADS), "crossover": self.get_parameter(CUOPT_CROSSOVER), "log_to_console": self.get_parameter(CUOPT_LOG_TO_CONSOLE), + "first_primal_feasible": self.get_parameter( + CUOPT_FIRST_PRIMAL_FEASIBLE + ), + "log_file": self.get_parameter(CUOPT_LOG_FILE), + "per_constraint_residual": self.get_parameter( + CUOPT_PER_CONSTRAINT_RESIDUAL + ), + "save_best_primal_so_far": self.get_parameter( + CUOPT_SAVE_BEST_PRIMAL_SO_FAR + ), + "solution_file": self.get_parameter(CUOPT_SOLUTION_FILE), + "strict_infeasibility": self.get_parameter( + CUOPT_STRICT_INFEASIBILITY + ), + "user_problem_file": self.get_parameter(CUOPT_USER_PROBLEM_FILE), } - solver_config["tolerances"]["absolute_dual"] = self.get_parameter( - CUOPT_ABSOLUTE_DUAL_TOLERANCE - ) - solver_config["tolerances"]["relative_dual"] = self.get_parameter( - CUOPT_RELATIVE_DUAL_TOLERANCE - ) - solver_config["tolerances"]["absolute_primal"] = self.get_parameter( - CUOPT_ABSOLUTE_PRIMAL_TOLERANCE - ) - solver_config["tolerances"]["relative_primal"] = self.get_parameter( - CUOPT_RELATIVE_PRIMAL_TOLERANCE - ) - solver_config["tolerances"]["absolute_gap"] = self.get_parameter( - CUOPT_ABSOLUTE_GAP_TOLERANCE - ) - solver_config["tolerances"]["relative_gap"] = self.get_parameter( - CUOPT_RELATIVE_GAP_TOLERANCE - ) - solver_config["tolerances"]["primal_infeasible"] = self.get_parameter( - CUOPT_PRIMAL_INFEASIBLE_TOLERANCE - ) - solver_config["tolerances"]["dual_infeasible"] = self.get_parameter( - CUOPT_DUAL_INFEASIBLE_TOLERANCE - ) - solver_config["tolerances"][ - "integrality_tolerance" - ] = self.get_parameter(CUOPT_MIP_INTEGRALITY_TOLERANCE) - solver_config["tolerances"]["absolute_mip_gap"] = self.get_parameter( - CUOPT_MIP_ABSOLUTE_GAP - ) - solver_config["tolerances"]["relative_mip_gap"] = self.get_parameter( - CUOPT_MIP_RELATIVE_GAP - ) + return solver_config diff --git a/python/cuopt_self_hosted/cuopt_sh_client/thin_client_solver_settings.py b/python/cuopt_self_hosted/cuopt_sh_client/thin_client_solver_settings.py index 9d21399319..63703b3e4d 100644 --- a/python/cuopt_self_hosted/cuopt_sh_client/thin_client_solver_settings.py +++ b/python/cuopt_self_hosted/cuopt_sh_client/thin_client_solver_settings.py @@ -163,53 +163,42 @@ def toDict(self): "tolerances": {}, } + t = [ + "absolute_primal_tolerance", + "absolute_dual_tolerance", + "absolute_gap_tolerance", + "relative_primal_tolerance", + "relative_dual_tolerance", + "relative_gap_tolerance", + "primal_infeasible_tolerance", + "dual_infeasible_tolerance", + "mip_integrality_tolerance", + "mip_absolute_gap", + "mip_relative_gap", + "mip_absolute_tolerance", + "mip_relative_tolerance", + # deprecated parameters + "absolute_primal", + "absolute_dual", + "absolute_gap", + "relative_primal", + "relative_dual", + "relative_gap", + "primal_infeasible", + "dual_infeasible", + "integrality_tolerance", + "absolute_mip_gap", + "relative_mip_gap", + ] + # Grab everything that is not a tolerance for key in self.parameter_dict: - if "tolerance" not in key: + if key not in t: solver_config[key] = self.parameter_dict[key] - # Handle tolerance separately - if "absolute_dual_tolerance" in self.parameter_dict: - solver_config["tolerances"]["absolute_dual"] = self.parameter_dict[ - "absolute_dual_tolerance" - ] - if "relative_dual_tolerance" in self.parameter_dict: - solver_config["tolerances"]["relative_dual"] = self.parameter_dict[ - "relative_dual_tolerance" - ] - if "absolute_primal_tolerance" in self.parameter_dict: - solver_config["tolerances"][ - "absolute_primal" - ] = self.parameter_dict["absolute_primal_tolerance"] - if "relative_primal_tolerance" in self.parameter_dict: - solver_config["tolerances"][ - "relative_primal" - ] = self.parameter_dict["relative_primal_tolerance"] - if "absolute_gap_tolerance" in self.parameter_dict: - solver_config["tolerances"]["absolute_gap"] = self.parameter_dict[ - "absolute_gap_tolerance" - ] - if "relative_gap_tolerance" in self.parameter_dict: - solver_config["tolerances"]["relative_gap"] = self.parameter_dict[ - "relative_gap_tolerance" - ] - if "primal_infeasible_tolerance" in self.parameter_dict: - solver_config["tolerances"][ - "primal_infeasible" - ] = self.parameter_dict["primal_infeasible_tolerance"] - if "dual_infeasible_tolerance" in self.parameter_dict: - solver_config["tolerances"][ - "dual_infeasible" - ] = self.parameter_dict["dual_infeasible_tolerance"] - if "integrality_tolerance" in self.parameter_dict: - solver_config["tolerances"][ - "integrality_tolerance" - ] = self.parameter_dict["integrality_tolerance"] - if "absolute_mip_gap" in self.parameter_dict: - solver_config["tolerances"][ - "absolute_mip_gap" - ] = self.parameter_dict["absolute_mip_gap"] - if "relative_mip_gap" in self.parameter_dict: - solver_config["tolerances"][ - "relative_mip_gap" - ] = self.parameter_dict["relative_mip_gap"] + + # Now grab everything that is a tolerance and set in the dictionary + for name in t: + if name in self.parameter_dict: + solver_config["tolerances"][name] = self.parameter_dict[name] + return solver_config diff --git a/python/cuopt_server/cuopt_server/utils/job_queue.py b/python/cuopt_server/cuopt_server/utils/job_queue.py index 7d1986e56c..5d2adc6a02 100644 --- a/python/cuopt_server/cuopt_server/utils/job_queue.py +++ b/python/cuopt_server/cuopt_server/utils/job_queue.py @@ -60,47 +60,6 @@ class PickleForbidden(Exception): msgpack_numpy.patch() -def lp_datamodel_compat(data): - """ - Maintain backward compat for some parameters - that change names in 25.05. Replace the - old parameters with the new names - """ - - sc = { - "solver_mode": "pdlp_solver_mode", - "heuristics_only": "mip_heuristics_only", - } - - tol = { - "integrality_tolerance": "mip_integrality_tolerance", - "absolute_mip_gap": "mip_absolute_gap", - "relative_mip_gap": "mip_relative_gap", - } - - replace = [] - if "solver_config" in data: - s = data["solver_config"] - for k, v in sc.items(): - if k in s: - replace.append([k, v, s[k]]) - - for r in replace: - data["solver_config"][r[1]] = r[2] - del data["solver_config"][r[0]] - - replace = [] - if "tolerances" in s: - t = s["tolerances"] - for k, v in tol.items(): - if k in t: - replace.append([k, v, t[k]]) - - for r in replace: - data["solver_config"]["tolerances"][r[1]] = r[2] - del data["solver_config"]["tolerances"][r[0]] - - def check_client_version(client_vers): logging.debug(f"client_vers is {client_vers} in check") if os.environ.get("CUOPT_CHECK_CLIENT", True) in ["True", True]: @@ -1289,7 +1248,6 @@ def _resolve_job(self): t = SolverLPJob(0, i_data, None, None) t._transform(t.LP_data) i_data = t.get_data() - lp_datamodel_compat(i_data) lpdata.append(LPData.parse_obj(i_data)) data = lpdata else: @@ -1299,7 +1257,6 @@ def _resolve_job(self): t = SolverLPJob(0, data, None, None) t._transform(t.LP_data) data = t.get_data() - lp_datamodel_compat(data) data = LPData.parse_obj(data) except Exception as e: raise HTTPException( @@ -1539,7 +1496,6 @@ def _resolve_job(self): t = SolverLPJob(0, i_data, None, None) t._transform(t.LP_data) i_data = t.get_data() - lp_datamodel_compat(i_data) lpdata.append(LPData.parse_obj(i_data)) data = lpdata else: @@ -1549,7 +1505,6 @@ def _resolve_job(self): t = SolverLPJob(0, data, None, None) t._transform(t.LP_data) data = t.get_data() - lp_datamodel_compat(data) data = LPData.parse_obj(data) except Exception as e: raise HTTPException( diff --git a/python/cuopt_server/cuopt_server/utils/linear_programming/data_definition.py b/python/cuopt_server/cuopt_server/utils/linear_programming/data_definition.py index d66b7c8171..c739c8b7e8 100644 --- a/python/cuopt_server/cuopt_server/utils/linear_programming/data_definition.py +++ b/python/cuopt_server/cuopt_server/utils/linear_programming/data_definition.py @@ -336,34 +336,34 @@ class Tolerances(StrictModel): default=None, description="absolute and relative tolerance on the primal feasibility, dual feasibility, and gap", # noqa ) - absolute_primal: float = Field( + absolute_primal_tolerance: float = Field( default=None, description="Absolute primal tolerance" ) - absolute_dual: float = Field( + absolute_dual_tolerance: float = Field( default=None, description="Absolute dual tolerance" "NOTE: Only applicable to LP", ) - absolute_gap: float = Field( + absolute_gap_tolerance: float = Field( default=None, description="Absolute gap tolerance" "NOTE: Only applicable to LP", ) - relative_primal: float = Field( + relative_primal_tolerance: float = Field( default=None, description="Relative primal tolerance" ) - relative_dual: float = Field( + relative_dual_tolerance: float = Field( default=None, description="Relative dual tolerance" "NOTE: Only applicable to LP", ) - relative_gap: float = Field( + relative_gap_tolerance: float = Field( default=None, description="Relative gap tolerance" "NOTE: Only applicable to LP", ) - primal_infeasible: float = Field( + primal_infeasible_tolerance: float = Field( default=None, description="Primal infeasible tolerance" "NOTE: Only applicable to LP", ) - dual_infeasible: float = Field( + dual_infeasible_tolerance: float = Field( default=None, description="Dual infeasible tolerance" "NOTE: Only applicable to LP", ) @@ -381,6 +381,78 @@ class Tolerances(StrictModel): description="MIP gap relative tolerance" "NOTE: Only applicable to MILP", ) + mip_absolute_tolerance: float = Field( + default=None, description="MIP absolute tolerance" + ) + mip_relative_tolerance: float = Field( + default=None, description="MIP relative tolerance" + ) + absolute_primal: float = Field( + default=None, + deprecated=True, + description="Deprecated in 25.08. " + "Use absolute_primal_tolerance instead", + ) + absolute_dual: float = Field( + default=None, + deprecated=True, + description="Deprecated in 25.08. " + "Use absolute_dual_tolerance instead", + ) + absolute_gap: float = Field( + default=None, + deprecated=True, + description="Deprecated in 25.08. " + "Use absolute_gap_tolerance instead", + ) + relative_primal: float = Field( + default=None, + deprecated=True, + description="Deprecated in 25.08. " + "Use relative_primal_tolerance instead", + ) + relative_dual: float = Field( + default=None, + deprecated=True, + description="Deprecated in 25.08. " + "Use relative_dual_tolerance instead", + ) + relative_gap: float = Field( + default=None, + deprecated=True, + description="Deprecated in 25.08. " + "Use relative_gap_tolerance instead", + ) + primal_infeasible: float = Field( + default=None, + deprecated=True, + description="Deprecated in 25.08. " + "Use primal_infeasible_tolerance instead", + ) + dual_infeasible: float = Field( + default=None, + deprecated=True, + description="Deprecated in 25.08. " + "Use dual_infeasible_tolerance instead", + ) + integrality_tolerance: float = Field( + default=None, + deprecated=True, + description="Deprecated starting in 25.05. " + "Use mip_integratlity_tolerance instead.", + ) + absolute_mip_gap: float = Field( + default=None, + deprecated=True, + description="Deprecated starting in 25.05. " + "Use mip_absolute_gap instead.", + ) + relative_mip_gap: float = Field( + default=None, + deprecated=True, + description="Deprecated starting in 25.05. " + "Use mip_relative_gap instead.", + ) class SolverConfig(StrictModel): @@ -468,6 +540,66 @@ class SolverConfig(StrictModel): description="Set True to write logs to console, False to " "not write logs to console.", ) + strict_infeasibility: Optional[bool] = Field( + default=False, + description=" controls the strict infeasibility " + "mode in PDLP. When true if either the current or " + "the average solution is detected as infeasible, " + "PDLP will stop. When false both the current and " + "average solution need to be detected as infeasible " + "for PDLP to stop.", + ) + user_problem_file: Optional[str] = Field( + default="", + description="Ignored by the service but included " + "for dataset compatibility", + ) + per_constraint_residual: Optional[bool] = Field( + default=False, + description="Controls whether PDLP should compute the " + "primal & dual residual per constraint instead of globally.", + ) + save_best_primal_so_far: Optional[bool] = Field( + default=False, + description="controls whether PDLP should save the " + "best primal solution so far. " + "With this parameter set to true, PDLP will always " + "prioritize a primal feasible " + "to a non primal feasible. " + "If a new primal feasible is found, the one with the " + "best primal objective will be kept. " + "If no primal feasible was found, the one " + "with the lowest primal residual will be kept. " + "If two have the same primal residual, " + "the one with the best objective will be kept.", + ) + first_primal_feasible: Optional[bool] = Field( + default=False, + description="Controls whether PDLP should stop when " + "the first primal feasible solution is found.", + ) + log_file: Optional[str] = Field( + default="", + description="Ignored by the service but included " + "for dataset compatibility", + ) + solution_file: Optional[str] = Field( + default="", + description="Ignored by the service but included " + "for dataset compatibility", + ) + solver_mode: Optional[int] = Field( + default=None, + deprecated=True, + description="Deprecated starting in 25.05. " + "Use pdlp_solver_mode instead.", + ) + heuristics_only: Optional[bool] = Field( + default=None, + deprecated=True, + description="Deprecated starting in 25.05. " + "Use mip_heuristics_only instead.", + ) class LPData(StrictModel): diff --git a/python/cuopt_server/cuopt_server/utils/linear_programming/solver.py b/python/cuopt_server/cuopt_server/utils/linear_programming/solver.py index fcb9d07649..ccb5b15149 100644 --- a/python/cuopt_server/cuopt_server/utils/linear_programming/solver.py +++ b/python/cuopt_server/cuopt_server/utils/linear_programming/solver.py @@ -27,21 +27,28 @@ CUOPT_ABSOLUTE_PRIMAL_TOLERANCE, CUOPT_CROSSOVER, CUOPT_DUAL_INFEASIBLE_TOLERANCE, + CUOPT_FIRST_PRIMAL_FEASIBLE, CUOPT_INFEASIBILITY_DETECTION, CUOPT_ITERATION_LIMIT, + CUOPT_LOG_FILE, CUOPT_LOG_TO_CONSOLE, CUOPT_METHOD, CUOPT_MIP_ABSOLUTE_GAP, + CUOPT_MIP_ABSOLUTE_TOLERANCE, CUOPT_MIP_HEURISTICS_ONLY, CUOPT_MIP_INTEGRALITY_TOLERANCE, CUOPT_MIP_RELATIVE_GAP, + CUOPT_MIP_RELATIVE_TOLERANCE, CUOPT_MIP_SCALING, CUOPT_NUM_CPU_THREADS, CUOPT_PDLP_SOLVER_MODE, + CUOPT_PER_CONSTRAINT_RESIDUAL, CUOPT_PRIMAL_INFEASIBLE_TOLERANCE, CUOPT_RELATIVE_DUAL_TOLERANCE, CUOPT_RELATIVE_GAP_TOLERANCE, CUOPT_RELATIVE_PRIMAL_TOLERANCE, + CUOPT_SAVE_BEST_PRIMAL_SO_FAR, + CUOPT_STRICT_INFEASIBILITY, CUOPT_TIME_LIMIT, ) from cuopt.linear_programming.solver.solver_wrapper import ( @@ -55,9 +62,16 @@ OutOfMemoryError, ) -dep_warning = ( - "{field} is deprecated and will be removed in the next release. Ignored." -) + +def dep_warning(field): + return ( + f"solver config {field} is deprecated and will " + "be removed in a future release" + ) + + +def ignored_warning(field): + return f"solver config {field} ignored in the cuopt service" class CustomGetSolutionCallback(GetSolutionCallback): @@ -156,7 +170,15 @@ def create_solver(LP_data, warmstart_data): CUOPT_INFEASIBILITY_DETECTION, solver_config.infeasibility_detection, ) - if solver_config.pdlp_solver_mode is not None: + if solver_config.solver_mode is not None: + solver_settings.set_parameter( + CUOPT_PDLP_SOLVER_MODE, + linear_programming.solver_settings.PDLPSolverMode( + solver_config.solver_mode + ), + ) + warnings.append(dep_warning("solver_mode")) + elif solver_config.pdlp_solver_mode is not None: solver_settings.set_parameter( CUOPT_PDLP_SOLVER_MODE, linear_programming.solver_settings.PDLPSolverMode( @@ -212,59 +234,138 @@ def create_solver(LP_data, warmstart_data): tolerance = solver_config.tolerances if tolerance.optimality is not None: solver_settings.set_optimality_tolerance(tolerance.optimality) - if tolerance.absolute_dual is not None: + if tolerance.absolute_dual_tolerance is not None: + solver_settings.set_parameter( + CUOPT_ABSOLUTE_DUAL_TOLERANCE, + tolerance.absolute_dual_tolerance, + ) + elif tolerance.absolute_dual is not None: solver_settings.set_parameter( CUOPT_ABSOLUTE_DUAL_TOLERANCE, tolerance.absolute_dual ) - if tolerance.absolute_primal is not None: + warnings.append(dep_warning("absolute_dual")) + if tolerance.absolute_primal_tolerance is not None: + solver_settings.set_parameter( + CUOPT_ABSOLUTE_PRIMAL_TOLERANCE, + tolerance.absolute_primal_tolerance, + ) + elif tolerance.absolute_primal is not None: solver_settings.set_parameter( CUOPT_ABSOLUTE_PRIMAL_TOLERANCE, tolerance.absolute_primal ) - if tolerance.absolute_gap is not None: + warnings.append(dep_warning("absolute_primal")) + if tolerance.absolute_gap_tolerance is not None: + solver_settings.set_parameter( + CUOPT_ABSOLUTE_GAP_TOLERANCE, + tolerance.absolute_gap_tolerance, + ) + elif tolerance.absolute_gap is not None: solver_settings.set_parameter( CUOPT_ABSOLUTE_GAP_TOLERANCE, tolerance.absolute_gap ) - if tolerance.relative_dual is not None: + warnings.append(dep_warning("absolute_gap")) + if tolerance.relative_dual_tolerance is not None: + solver_settings.set_parameter( + CUOPT_RELATIVE_DUAL_TOLERANCE, + tolerance.relative_dual_tolerance, + ) + elif tolerance.relative_dual is not None: solver_settings.set_parameter( CUOPT_RELATIVE_DUAL_TOLERANCE, tolerance.relative_dual ) - if tolerance.relative_primal is not None: + warnings.append(dep_warning("relative_dual")) + if tolerance.relative_primal_tolerance is not None: + solver_settings.set_parameter( + CUOPT_RELATIVE_PRIMAL_TOLERANCE, + tolerance.relative_primal_tolerance, + ) + elif tolerance.relative_primal is not None: solver_settings.set_parameter( CUOPT_RELATIVE_PRIMAL_TOLERANCE, tolerance.relative_primal ) - if tolerance.relative_gap is not None: + warnings.append(dep_warning("relative_primal")) + if tolerance.relative_gap_tolerance is not None: + solver_settings.set_parameter( + CUOPT_RELATIVE_GAP_TOLERANCE, + tolerance.relative_gap_tolerance, + ) + elif tolerance.relative_gap is not None: solver_settings.set_parameter( CUOPT_RELATIVE_GAP_TOLERANCE, tolerance.relative_gap ) - if tolerance.primal_infeasible is not None: + warnings.append(dep_warning("relative_gap")) + if tolerance.primal_infeasible_tolerance is not None: + solver_settings.set_parameter( + CUOPT_PRIMAL_INFEASIBLE_TOLERANCE, + tolerance.primal_infeasible_tolerance, + ) + elif tolerance.primal_infeasible is not None: solver_settings.set_parameter( CUOPT_PRIMAL_INFEASIBLE_TOLERANCE, tolerance.primal_infeasible, ) - if tolerance.dual_infeasible is not None: + warnings.append(dep_warning("primal_infeasible")) + if tolerance.dual_infeasible_tolerance is not None: + solver_settings.set_parameter( + CUOPT_DUAL_INFEASIBLE_TOLERANCE, + tolerance.dual_infeasible_tolerance, + ) + elif tolerance.dual_infeasible is not None: solver_settings.set_parameter( CUOPT_DUAL_INFEASIBLE_TOLERANCE, tolerance.dual_infeasible ) + warnings.append(dep_warning("dual_infeasible")) if tolerance.mip_integrality_tolerance is not None: solver_settings.set_parameter( CUOPT_MIP_INTEGRALITY_TOLERANCE, tolerance.mip_integrality_tolerance, ) + elif tolerance.integrality_tolerance is not None: + solver_settings.set_parameter( + CUOPT_MIP_INTEGRALITY_TOLERANCE, + tolerance.integrality_tolerance, + ) + warnings.append(dep_warning("integrality_tolerance")) if tolerance.mip_absolute_gap is not None: solver_settings.set_parameter( CUOPT_MIP_ABSOLUTE_GAP, tolerance.mip_absolute_gap ) + elif tolerance.absolute_mip_gap is not None: + solver_settings.set_parameter( + CUOPT_MIP_ABSOLUTE_GAP, tolerance.absolute_mip_gap + ) + warnings.append(dep_warning("absolute_mip_gap")) if tolerance.mip_relative_gap is not None: solver_settings.set_parameter( CUOPT_MIP_RELATIVE_GAP, tolerance.mip_relative_gap ) + elif tolerance.relative_mip_gap is not None: + solver_settings.set_parameter( + CUOPT_MIP_RELATIVE_GAP, tolerance.relative_mip_gap + ) + warnings.append(dep_warning("relative_mip_gap")) + if tolerance.mip_absolute_tolerance is not None: + solver_settings.set_parameter( + CUOPT_MIP_ABSOLUTE_TOLERANCE, + tolerance.mip_absolute_tolerance, + ) + if tolerance.mip_relative_tolerance is not None: + solver_settings.set_parameter( + CUOPT_MIP_RELATIVE_TOLERANCE, + tolerance.mip_relative_tolerance, + ) if warmstart_data is not None: solver_settings.set_pdlp_warm_start_data(warmstart_data) if solver_config.mip_scaling is not None: solver_settings.set_parameter( CUOPT_MIP_SCALING, solver_config.mip_scaling ) - if solver_config.mip_heuristics_only is not None: + if solver_config.heuristics_only is not None: + solver_settings.set_parameter( + CUOPT_MIP_HEURISTICS_ONLY, solver_config.heuristics_only + ) + warnings.append(dep_warning("heuristics_only")) + elif solver_config.mip_heuristics_only is not None: solver_settings.set_parameter( CUOPT_MIP_HEURISTICS_ONLY, solver_config.mip_heuristics_only ) @@ -280,6 +381,34 @@ def create_solver(LP_data, warmstart_data): solver_settings.set_parameter( CUOPT_LOG_TO_CONSOLE, solver_config.log_to_console ) + if solver_config.strict_infeasibility is not None: + solver_settings.set_parameter( + CUOPT_STRICT_INFEASIBILITY, solver_config.strict_infeasibility + ) + if solver_config.user_problem_file != "": + warnings.append(ignored_warning("user_problem_file")) + if solver_config.per_constraint_residual is not None: + solver_settings.set_parameter( + CUOPT_PER_CONSTRAINT_RESIDUAL, + solver_config.per_constraint_residual, + ) + if solver_config.save_best_primal_so_far is not None: + solver_settings.set_parameter( + CUOPT_SAVE_BEST_PRIMAL_SO_FAR, + solver_config.save_best_primal_so_far, + ) + if solver_config.first_primal_feasible is not None: + solver_settings.set_parameter( + CUOPT_FIRST_PRIMAL_FEASIBLE, + solver_config.first_primal_feasible, + ) + if solver_config.log_file != "": + solver_settings.set_parameter( + CUOPT_LOG_FILE, solver_config.log_file + ) + if solver_config.solution_file != "": + warnings.append(ignored_warning("solution_file")) + return warnings, solver_settings @@ -300,7 +429,7 @@ def get_solver_exception_type(status, message): return RuntimeError(msg) -def solve(LP_data, reqId, intermediate_sender, warmstart_data, log_file): +def solve(LP_data, reqId, intermediate_sender, warmstart_data): notes = [] def get_if_attribute_is_valid_else_none(attr): @@ -431,7 +560,7 @@ def create_solution(sol): solver_settings.set_mip_callback(callback) solve_begin_time = time.time() sol = linear_programming.Solve( - data_model, solver_settings=solver_settings, log_file=log_file + data_model, solver_settings=solver_settings ) total_solve_time = time.time() - solve_begin_time diff --git a/python/cuopt_server/cuopt_server/utils/solver.py b/python/cuopt_server/cuopt_server/utils/solver.py index 0a98c368f9..5c09b07f9e 100644 --- a/python/cuopt_server/cuopt_server/utils/solver.py +++ b/python/cuopt_server/cuopt_server/utils/solver.py @@ -88,7 +88,7 @@ def solve_LP_sync( begin_time = time.time() - if type(LP_data) is list: + if isinstance(LP_data, list): for i_data in LP_data: validate_LP_data(i_data) else: @@ -98,15 +98,31 @@ def solve_LP_sync( logging.debug(f"etl_time {etl_end_time - begin_time}") if not validation_only: - if solver_logging: + # log_file setting is ignored in the service, + # instead we control it and use it as the basis for callbacks + if isinstance(LP_data, list): + # clear log_file setting for all because + # we don't support callbacks for batch mode + # and otherwise we ignore log_file + for i_data in LP_data: + i_data.solver_config.log_file = "" + elif solver_logging: log_dir, _, _ = settings.get_result_dir() log_fname = "log_" + reqId log_file = os.path.join(log_dir, log_fname) logging.info(f"Writing logs to {log_file}") - else: - log_file = "" + LP_data.solver_config.log_file = log_file + elif LP_data.solver_config.log_file: + warnings.append( + "solver config log_file ignored in the cuopt service" + ) + LP_data.solver_config.log_file = "" + notes, addl_warnings, res, total_solve_time = LP_solve( - LP_data, reqId, intermediate_sender, warmstart_data, log_file + LP_data, + reqId, + intermediate_sender, + warmstart_data, ) warnings.extend(addl_warnings) else: diff --git a/python/cuopt_server/cuopt_server/webserver.py b/python/cuopt_server/cuopt_server/webserver.py index edbc8234dc..ac389652a6 100644 --- a/python/cuopt_server/cuopt_server/webserver.py +++ b/python/cuopt_server/cuopt_server/webserver.py @@ -958,7 +958,7 @@ async def postrequest( ), solver_logs: Optional[bool] = Query( default=False, - description="If set to True, MIP problems will produce detailed solver logs that can be retrieved from /cuopt/log/{id}", # noqa + description="If set to True, math optimization problems will produce detailed solver logs that can be retrieved from /cuopt/log/{id}. ", # noqa ), cuopt_data_file: str = Header( default=None,