From 9cfe2d9678bf3a3c83438441abe87918b38d628a Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Wed, 23 Jul 2025 10:14:53 -0400 Subject: [PATCH 01/20] align service api with SDK (compat with deprecations) --- .../linear_programming/data_definition.py | 129 ++++++++++++++++-- .../utils/linear_programming/solver.py | 69 ++++++++-- 2 files changed, 182 insertions(+), 16 deletions(-) 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..9d96116216 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,61 @@ class Tolerances(StrictModel): description="MIP gap relative tolerance" "NOTE: Only applicable to MILP", ) + absolute_primal: float = Field( + default=None, + description="Deprecated in 25.08. " + "Use absolute_primal_tolerance instead", + ) + absolute_dual: float = Field( + default=None, + description="Deprecated in 25.08. " + "Use absolute_dual_tolerance instead", + ) + absolute_gap: float = Field( + default=None, + description="Deprecated in 25.08. " + "Use absolute_gap_tolerance instead", + ) + relative_primal: float = Field( + default=None, + description="Deprecated in 25.08. " + "Use relative_primal_tolerance instead", + ) + relative_dual: float = Field( + default=None, + description="Deprecated in 25.08. " + "Use relative_dual_tolerance instead", + ) + relative_gap: float = Field( + default=None, + description="Deprecated in 25.08. " + "Use relative_gap_tolerance instead", + ) + primal_infeasible: float = Field( + default=None, + description="Deprecated in 25.08. " + "Use primal_infeasible_tolerance instead", + ) + dual_infeasible: float = Field( + default=None, + description="Deprecated in 25.08. " + "Use dual_infeasible_tolerance instead", + ) + integrality_tolerance: float = Field( + default=None, + description="Deprecated starting in 25.05. " + "Use mip_integratlity_tolerance instead.", + ) + absolute_mip_gap: float = Field( + default=None, + description="Deprecated starting in 25.05. " + "Use mip_absolute_gap instead.", + ) + relative_mip_gap: float = Field( + default=None, + description="Deprecated starting in 25.05. " + "Use mip_relative_gap instead.", + ) class SolverConfig(StrictModel): @@ -468,6 +523,64 @@ 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="Controls the name of a file where " + "cuOpt should write the user problem.", + ) + 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="Controls the name of a log file where cuOpt " + "should write information about the solve.", + ) + solution_file: Optional[str] = Field( + default="", + description="Controls the name of a file where " + "cuOpt should write the solution.", + ) + solver_mode: Optional[int] = Field( + default=None, + description="Deprecated starting in 25.05. " + "Use pdlp_solver_mode instead.", + ) + heuristics_only: Optional[bool] = Field( + default=None, + 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..ed3667320f 100644 --- a/python/cuopt_server/cuopt_server/utils/linear_programming/solver.py +++ b/python/cuopt_server/cuopt_server/utils/linear_programming/solver.py @@ -212,36 +212,76 @@ 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: + 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: + 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: + 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: + 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: + 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: + 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: + 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 ) @@ -250,14 +290,27 @@ def create_solver(LP_data, warmstart_data): 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, + ) 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 + ) 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 + ) if warmstart_data is not None: solver_settings.set_pdlp_warm_start_data(warmstart_data) if solver_config.mip_scaling is not None: From 9146959c3971fa16832a3c6c013d373939f6b0c9 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Wed, 23 Jul 2025 14:53:19 -0400 Subject: [PATCH 02/20] cuopt server add missing parameters to align with SDK --- .../utils/linear_programming/solver.py | 54 ++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) 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 ed3667320f..913835ea05 100644 --- a/python/cuopt_server/cuopt_server/utils/linear_programming/solver.py +++ b/python/cuopt_server/cuopt_server/utils/linear_programming/solver.py @@ -27,8 +27,10 @@ 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, @@ -38,11 +40,16 @@ 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, ) from cuopt.linear_programming.solver.solver_wrapper import ( ErrorStatus, @@ -156,7 +163,14 @@ 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 + ), + ) + elif solver_config.pdlp_solver_mode is not None: solver_settings.set_parameter( CUOPT_PDLP_SOLVER_MODE, linear_programming.solver_settings.PDLPSolverMode( @@ -317,7 +331,11 @@ def create_solver(LP_data, warmstart_data): 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 + ) + elif solver_config.mip_heuristics_only is not None: solver_settings.set_parameter( CUOPT_MIP_HEURISTICS_ONLY, solver_config.mip_heuristics_only ) @@ -333,6 +351,38 @@ 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 != "": + solver_settings.set_parameter( + CUOPT_USER_PROBLEM_FILE, solver_config.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 != "": + solver_settings.set_parameter( + CUOPT_SOLUTION_FILE, solver_config.solution_file + ) + return warnings, solver_settings From c045f15dcc6be588e3308b07adf81150f13274bc Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Wed, 23 Jul 2025 15:07:58 -0400 Subject: [PATCH 03/20] cuopt server get rid of lp_datamodel_compat for solver configs --- .../cuopt_server/utils/job_queue.py | 45 ------------------- 1 file changed, 45 deletions(-) diff --git a/python/cuopt_server/cuopt_server/utils/job_queue.py b/python/cuopt_server/cuopt_server/utils/job_queue.py index 31fed39126..fa0e68078a 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( From 5c038a7888182e06da6d2461ab0031d4eab97e1c Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Wed, 23 Jul 2025 17:00:16 -0400 Subject: [PATCH 04/20] add missing parameters to service and solver_settings.py Also correct lp-milp-settings.rst --- docs/cuopt/source/lp-milp-settings.rst | 6 +- .../solver_settings/solver_settings.py | 102 +++++++++++------- .../linear_programming/data_definition.py | 6 ++ .../utils/linear_programming/solver.py | 12 +++ 4 files changed, 87 insertions(+), 39 deletions(-) diff --git a/docs/cuopt/source/lp-milp-settings.rst b/docs/cuopt/source/lp-milp-settings.rst index 6a5309a570..73e0a8e59b 100644 --- a/docs/cuopt/source/lp-milp-settings.rst +++ b/docs/cuopt/source/lp-milp-settings.rst @@ -43,7 +43,7 @@ Note: the default value is ``""`` and no log file is written. 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. @@ -273,14 +273,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/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_server/cuopt_server/utils/linear_programming/data_definition.py b/python/cuopt_server/cuopt_server/utils/linear_programming/data_definition.py index 9d96116216..f4e43f54b5 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 @@ -381,6 +381,12 @@ 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, description="Deprecated in 25.08. " 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 913835ea05..6c2de01f01 100644 --- a/python/cuopt_server/cuopt_server/utils/linear_programming/solver.py +++ b/python/cuopt_server/cuopt_server/utils/linear_programming/solver.py @@ -34,9 +34,11 @@ 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, @@ -325,6 +327,16 @@ def create_solver(LP_data, warmstart_data): solver_settings.set_parameter( CUOPT_MIP_RELATIVE_GAP, tolerance.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: From f3e680cfaf306e9304dbf62b333faa863dbe9e62 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Thu, 24 Jul 2025 15:39:56 -0400 Subject: [PATCH 05/20] cuopt client fix up ThinClientSolverSettings --- .../thin_client_solver_settings.py | 67 ++++++------------- 1 file changed, 21 insertions(+), 46 deletions(-) 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 96e2e8cb93..3564eb4348 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,28 @@ 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", + ] + # 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 seperately - 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"] + + for name in t: + if name in self.parameter_dict: + solver_config["tolerances"][name] = self.parameter_dict[name] return solver_config From edaeef75294b206ec9bf73629d6ccda159949c82 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Fri, 25 Jul 2025 08:40:29 -0400 Subject: [PATCH 06/20] cuopt client include deprecated parameters in ThinClientSolverSettings --- .../cuopt_sh_client/thin_client_solver_settings.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 3564eb4348..48ca550fea 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 @@ -177,6 +177,18 @@ def toDict(self): "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 From 61bf06ff30003cc5734869fe303d86ddd18e3697 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Fri, 25 Jul 2025 10:32:48 -0400 Subject: [PATCH 07/20] cuopt service resolve conflict with log_file parameter and callbacks --- .../cuopt/linear_programming/solver/solver.py | 9 +++------ .../solver/solver_wrapper.pyx | 17 ++++------------- .../utils/linear_programming/solver.py | 4 ++-- .../cuopt_server/cuopt_server/utils/solver.py | 10 +++++++++- python/cuopt_server/cuopt_server/webserver.py | 4 +++- 5 files changed, 21 insertions(+), 23 deletions(-) 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_server/cuopt_server/utils/linear_programming/solver.py b/python/cuopt_server/cuopt_server/utils/linear_programming/solver.py index 6c2de01f01..6363d87345 100644 --- a/python/cuopt_server/cuopt_server/utils/linear_programming/solver.py +++ b/python/cuopt_server/cuopt_server/utils/linear_programming/solver.py @@ -415,7 +415,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): @@ -546,7 +546,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..a39ff50331 100644 --- a/python/cuopt_server/cuopt_server/utils/solver.py +++ b/python/cuopt_server/cuopt_server/utils/solver.py @@ -103,10 +103,18 @@ def solve_LP_sync( log_fname = "log_" + reqId log_file = os.path.join(log_dir, log_fname) logging.info(f"Writing logs to {log_file}") + # We have to potentially overwrite the user log_file + # value here since we are running in the service and using + # web callbacks. The user can not use callbacks if they + # want explicit control over the log file. + LP_data.solver_config.log_file = log_file else: 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 6c84445d2f..c7557b7b81 100644 --- a/python/cuopt_server/cuopt_server/webserver.py +++ b/python/cuopt_server/cuopt_server/webserver.py @@ -957,7 +957,9 @@ 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, MIP problems will produce detailed solver logs that can be retrieved from /cuopt/log/{id}. " # noqa + "The solver_config.log_file value will be set to log_{id} overriding any user value set. If a user wants logs generated " # noqa + "on the server with a user-defined name, then do not use the callback mechanism and simply set solver_config.log_file as desired.", # noqa ), cuopt_data_file: str = Header( default=None, From 6d4f724601b24482578f3a873937f23395b12a68 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Fri, 25 Jul 2025 13:30:43 -0400 Subject: [PATCH 08/20] docs change CUOPT_HEURISTICS_ONLY to CUOPT_MIP_HEURISTICS_ONLY --- docs/cuopt/source/lp-milp-settings.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cuopt/source/lp-milp-settings.rst b/docs/cuopt/source/lp-milp-settings.rst index 73e0a8e59b..195074992b 100644 --- a/docs/cuopt/source/lp-milp-settings.rst +++ b/docs/cuopt/source/lp-milp-settings.rst @@ -255,7 +255,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. From 2fe92acc8d6f3dd750886b0fc3904c6882ccdb2b Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Fri, 25 Jul 2025 14:07:43 -0400 Subject: [PATCH 09/20] add test for solver_config parameter names --- ci/test_self_hosted_service.sh | 4 + .../cuopt_service_data/lpmip_configs.json | 103 ++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 datasets/cuopt_service_data/lpmip_configs.json 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 + } +} From ca66e24ac9c4fdaf68fa7a93d86db5be5026c407 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Mon, 28 Jul 2025 16:08:50 -0400 Subject: [PATCH 10/20] cuopt docs add some clarity around logging and incumbents --- docs/cuopt/source/lp-features.rst | 14 +++++++++++--- docs/cuopt/source/milp-features.rst | 19 +++++++++++++------ python/cuopt_server/cuopt_server/webserver.py | 2 +- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/docs/cuopt/source/lp-features.rst b/docs/cuopt/source/lp-features.rst index 29a7e6f5c0..05a5478a50 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 `here `__. -Logging Callback ----------------- -With logging callback, users can fetch server-side logs for additional debugs and to get details on solver process details. `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. + +Logging Callback in the Service +------------------------------- + +In the cuOpt service API, the treatment of log files is slightly different. The ``log_file`` value in ``solver_configs`` may be set to specify the name of a log file on the server where logs will be written. The service will set the CUOPT_LOG_FILE parameter in the solver on your behalf. If you have access to the server, you may read the log files directly. The default location will be the directory where the service was launched. If a results directory is set when the service is launched, then log files will be written in the results directory. + +If however you set the ``solver_logs`` flag on the ``/cuopt/request`` REST API call, the value of the CUOPT_LOG_FILE parameter will always be set to ``log_{job_id}`` to support the logging callback feature of the service, and the ``log_file`` value in ``solver_configs`` will be ignored. With logging callback, users can fetch server-side logs for additional debugs and to get details on solver process details. `Examples `__ are shared on the self-hosted page. Infeasibility Detection diff --git a/docs/cuopt/source/milp-features.rst b/docs/cuopt/source/milp-features.rst index 17693a5b50..f554b1122e 100644 --- a/docs/cuopt/source/milp-features.rst +++ b/docs/cuopt/source/milp-features.rst @@ -50,15 +50,22 @@ 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 `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 `Incumbent Example `_ is shared on the self-hosted page. -Logging Callback ----------------- +Logging +------- -A logging callback allows users to get additional information about how the solve is progressing. A `Logging Callback Example `_ is shared on the self-hosted page. +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. + +Logging Callback in the Service +------------------------------- + +In the cuOpt service API, the treatment of log files is slightly different. The ``log_file`` value in ``solver_configs`` may be set to specify the name of a log file on the server where logs will be written. The service will set the CUOPT_LOG_FILE parameter in the solver on your behalf. If you have access to the server, you may read the log files directly. The default location will be the directory where the service was launched. If a results directory is set when the service is launched, then log files will be written in the results directory. + +If however you set the ``solver_logs`` flag on the ``/cuopt/request`` REST API call, the value of the CUOPT_LOG_FILE parameter will always be set to ``log_{job_id}`` to support the logging callback feature of the service, and the ``log_file`` value in ``solver_configs`` will be ignored. A logging callback allows users to get additional information about how the solve is progressing. A `Logging Callback Example `_ is shared on the self-hosted page. Time Limit -------------- diff --git a/python/cuopt_server/cuopt_server/webserver.py b/python/cuopt_server/cuopt_server/webserver.py index 8c1963f420..ade92463db 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 "The solver_config.log_file value will be set to log_{id} overriding any user value set. If a user wants logs generated " # noqa "on the server with a user-defined name, then do not use the callback mechanism and simply set solver_config.log_file as desired.", # noqa ), From 54a2e67928a5bee0fdaea08efd8920b4ff11c3df Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Mon, 28 Jul 2025 17:59:51 -0400 Subject: [PATCH 11/20] cuopt service add deprecation warnings for legacy solver_configs --- .../utils/linear_programming/solver.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) 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 6363d87345..58057ef78e 100644 --- a/python/cuopt_server/cuopt_server/utils/linear_programming/solver.py +++ b/python/cuopt_server/cuopt_server/utils/linear_programming/solver.py @@ -64,9 +64,9 @@ OutOfMemoryError, ) -dep_warning = ( - "{field} is deprecated and will be removed in the next release. Ignored." -) + +def dep_warning(field): + return f"{field} is deprecated and will " "be removed in a future release." class CustomGetSolutionCallback(GetSolutionCallback): @@ -172,6 +172,7 @@ def create_solver(LP_data, warmstart_data): 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, @@ -237,6 +238,7 @@ def create_solver(LP_data, warmstart_data): solver_settings.set_parameter( CUOPT_ABSOLUTE_DUAL_TOLERANCE, tolerance.absolute_dual ) + warnings.append(dep_warning("absolute_dual")) if tolerance.absolute_primal_tolerance is not None: solver_settings.set_parameter( CUOPT_ABSOLUTE_PRIMAL_TOLERANCE, @@ -246,6 +248,7 @@ def create_solver(LP_data, warmstart_data): solver_settings.set_parameter( CUOPT_ABSOLUTE_PRIMAL_TOLERANCE, tolerance.absolute_primal ) + warnings.append(dep_warning("absolute_primal")) if tolerance.absolute_gap_tolerance is not None: solver_settings.set_parameter( CUOPT_ABSOLUTE_GAP_TOLERANCE, @@ -255,6 +258,7 @@ def create_solver(LP_data, warmstart_data): solver_settings.set_parameter( CUOPT_ABSOLUTE_GAP_TOLERANCE, tolerance.absolute_gap ) + warnings.append(dep_warning("absolute_gap")) if tolerance.relative_dual_tolerance is not None: solver_settings.set_parameter( CUOPT_RELATIVE_DUAL_TOLERANCE, @@ -264,6 +268,7 @@ def create_solver(LP_data, warmstart_data): solver_settings.set_parameter( CUOPT_RELATIVE_DUAL_TOLERANCE, tolerance.relative_dual ) + warnings.append(dep_warning("relative_dual")) if tolerance.relative_primal_tolerance is not None: solver_settings.set_parameter( CUOPT_RELATIVE_PRIMAL_TOLERANCE, @@ -273,6 +278,7 @@ def create_solver(LP_data, warmstart_data): solver_settings.set_parameter( CUOPT_RELATIVE_PRIMAL_TOLERANCE, tolerance.relative_primal ) + warnings.append(dep_warning("relative_primal")) if tolerance.relative_gap_tolerance is not None: solver_settings.set_parameter( CUOPT_RELATIVE_GAP_TOLERANCE, @@ -282,6 +288,7 @@ def create_solver(LP_data, warmstart_data): solver_settings.set_parameter( CUOPT_RELATIVE_GAP_TOLERANCE, tolerance.relative_gap ) + warnings.append(dep_warning("relative_gap")) if tolerance.primal_infeasible_tolerance is not None: solver_settings.set_parameter( CUOPT_PRIMAL_INFEASIBLE_TOLERANCE, @@ -292,6 +299,7 @@ def create_solver(LP_data, warmstart_data): CUOPT_PRIMAL_INFEASIBLE_TOLERANCE, tolerance.primal_infeasible, ) + warnings.append(dep_warning("primal_infeasible")) if tolerance.dual_infeasible_tolerance is not None: solver_settings.set_parameter( CUOPT_DUAL_INFEASIBLE_TOLERANCE, @@ -301,6 +309,7 @@ def create_solver(LP_data, warmstart_data): 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, @@ -311,6 +320,7 @@ def create_solver(LP_data, warmstart_data): 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 @@ -319,6 +329,7 @@ def create_solver(LP_data, warmstart_data): 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 @@ -327,6 +338,7 @@ def create_solver(LP_data, warmstart_data): 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, @@ -347,6 +359,7 @@ def create_solver(LP_data, warmstart_data): 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 From 54f99d3c5c3060d60853c582af2b332907591a72 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Mon, 28 Jul 2025 18:13:27 -0400 Subject: [PATCH 12/20] update transition guide for solver_configs changes --- docs/cuopt/source/transition.rst | 64 ++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/docs/cuopt/source/transition.rst b/docs/cuopt/source/transition.rst index 97d48f7b71..029bf534c1 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,64 @@ 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, check the LP/MILP features guide or the service openapi spec. + +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 + +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 + + + From cdabf66873fd4cf005f5b9c476de5f3bb96e05c4 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Mon, 28 Jul 2025 17:24:26 -0500 Subject: [PATCH 13/20] fix style issues --- .../cuopt_sh_client/thin_client_solver_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e1fd4fcad6..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 @@ -200,5 +200,5 @@ def toDict(self): for name in t: if name in self.parameter_dict: solver_config["tolerances"][name] = self.parameter_dict[name] - + return solver_config From 6c89b6625fae8cc49dece7345072ccad1e82bdf2 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Mon, 28 Jul 2025 18:32:01 -0400 Subject: [PATCH 14/20] tweak transition guide --- docs/cuopt/source/transition.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/cuopt/source/transition.rst b/docs/cuopt/source/transition.rst index 029bf534c1..dd3d47bbf1 100644 --- a/docs/cuopt/source/transition.rst +++ b/docs/cuopt/source/transition.rst @@ -16,7 +16,10 @@ Parameter/option statuses are listed below, they express how each of these optio **Removed** - These features were deprecated in a previous release and completely removed in this one. -For all solver_configs fields, check the LP/MILP features guide or the service openapi spec. +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: @@ -51,11 +54,16 @@ The following fields are **New** in ``solver_configs.tolerances`` for the servic - 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 From ebd3cf57005cf53a575f257573ae83c5b65cdb31 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Mon, 28 Jul 2025 18:34:42 -0400 Subject: [PATCH 15/20] cuopt server add deprecated=True to solver_config fields --- .../utils/linear_programming/data_definition.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 f4e43f54b5..2bdcb461a9 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 @@ -389,56 +389,67 @@ class Tolerances(StrictModel): ) 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.", ) @@ -579,11 +590,13 @@ class SolverConfig(StrictModel): ) 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.", ) From 6c144735279db607f809ecde6f7f53d011bcf699 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Tue, 29 Jul 2025 12:27:51 -0400 Subject: [PATCH 16/20] cuopt service disable solver config file options --- docs/cuopt/source/lp-features.rst | 2 +- docs/cuopt/source/lp-milp-settings.rst | 6 +++--- docs/cuopt/source/milp-features.rst | 2 +- .../utils/linear_programming/solver.py | 19 ++++++++++--------- .../cuopt_server/cuopt_server/utils/solver.py | 8 ++++++-- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/docs/cuopt/source/lp-features.rst b/docs/cuopt/source/lp-features.rst index b82ce211d1..fdd6fce45b 100644 --- a/docs/cuopt/source/lp-features.rst +++ b/docs/cuopt/source/lp-features.rst @@ -79,7 +79,7 @@ Crossover allows you to obtain a high-quality basic solution from the results of 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. +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 ------------------------------- diff --git a/docs/cuopt/source/lp-milp-settings.rst b/docs/cuopt/source/lp-milp-settings.rst index 14a4a49f5c..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_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 ^^^^^^^^^^^^^^^ diff --git a/docs/cuopt/source/milp-features.rst b/docs/cuopt/source/milp-features.rst index 01db8c5966..42c2500850 100644 --- a/docs/cuopt/source/milp-features.rst +++ b/docs/cuopt/source/milp-features.rst @@ -58,7 +58,7 @@ When using the service, users can provide a callback to receive new integer feas 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. +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 ------------------------------- 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 58057ef78e..ccb5b15149 100644 --- a/python/cuopt_server/cuopt_server/utils/linear_programming/solver.py +++ b/python/cuopt_server/cuopt_server/utils/linear_programming/solver.py @@ -48,10 +48,8 @@ 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, ) from cuopt.linear_programming.solver.solver_wrapper import ( ErrorStatus, @@ -66,7 +64,14 @@ def dep_warning(field): - return f"{field} is deprecated and will " "be removed in a future release." + 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): @@ -381,9 +386,7 @@ def create_solver(LP_data, warmstart_data): CUOPT_STRICT_INFEASIBILITY, solver_config.strict_infeasibility ) if solver_config.user_problem_file != "": - solver_settings.set_parameter( - CUOPT_USER_PROBLEM_FILE, 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, @@ -404,9 +407,7 @@ def create_solver(LP_data, warmstart_data): CUOPT_LOG_FILE, solver_config.log_file ) if solver_config.solution_file != "": - solver_settings.set_parameter( - CUOPT_SOLUTION_FILE, solver_config.solution_file - ) + warnings.append(ignored_warning("solution_file")) return warnings, solver_settings diff --git a/python/cuopt_server/cuopt_server/utils/solver.py b/python/cuopt_server/cuopt_server/utils/solver.py index a39ff50331..863442f2ae 100644 --- a/python/cuopt_server/cuopt_server/utils/solver.py +++ b/python/cuopt_server/cuopt_server/utils/solver.py @@ -108,8 +108,12 @@ def solve_LP_sync( # web callbacks. The user can not use callbacks if they # want explicit control over the log file. LP_data.solver_config.log_file = log_file - else: - 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, From 16499d405dac8d070044e3108636d8f6e7fd830f Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Tue, 29 Jul 2025 12:45:08 -0400 Subject: [PATCH 17/20] cuopt service fix up docs related to log_file solver config --- docs/cuopt/source/milp-features.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/cuopt/source/milp-features.rst b/docs/cuopt/source/milp-features.rst index 42c2500850..3891561084 100644 --- a/docs/cuopt/source/milp-features.rst +++ b/docs/cuopt/source/milp-features.rst @@ -63,9 +63,10 @@ The CUOPT_LOG_FILE parameter can be set to write detailed solver logs for MILP p Logging Callback in the Service ------------------------------- -In the cuOpt service API, the treatment of log files is slightly different. The ``log_file`` value in ``solver_configs`` may be set to specify the name of a log file on the server where logs will be written. The service will set the CUOPT_LOG_FILE parameter in the solver on your behalf. If you have access to the server, you may read the log files directly. The default location will be the directory where the service was launched. If a results directory is set when the service is launched, then log files will be written in the results directory. +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. -If however you set the ``solver_logs`` flag on the ``/cuopt/request`` REST API call, the value of the CUOPT_LOG_FILE parameter will always be set to ``log_{job_id}`` to support the logging callback feature of the service, and the ``log_file`` value in ``solver_configs`` will be ignored. 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 -------------- From 09ad0ba205473dc0796389af5a0101be6a6fc185 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Tue, 29 Jul 2025 12:45:55 -0400 Subject: [PATCH 18/20] cuopt service fix up docs on log_file solver config --- docs/cuopt/source/lp-features.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cuopt/source/lp-features.rst b/docs/cuopt/source/lp-features.rst index fdd6fce45b..e89ee8f00b 100644 --- a/docs/cuopt/source/lp-features.rst +++ b/docs/cuopt/source/lp-features.rst @@ -84,9 +84,9 @@ The CUOPT_LOG_FILE parameter can be set to write detailed solver logs for LP pro Logging Callback in the Service ------------------------------- -In the cuOpt service API, the treatment of log files is slightly different. The ``log_file`` value in ``solver_configs`` may be set to specify the name of a log file on the server where logs will be written. The service will set the CUOPT_LOG_FILE parameter in the solver on your behalf. If you have access to the server, you may read the log files directly. The default location will be the directory where the service was launched. If a results directory is set when the service is launched, then log files will be written in the results directory. +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, the value of the CUOPT_LOG_FILE parameter will always be set to ``log_{job_id}`` to support the logging callback feature of the service, and the ``log_file`` value in ``solver_configs`` will be ignored. 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. +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 From c6336be1d3dfdca1c2b51691be657239d1751cd1 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Tue, 29 Jul 2025 12:58:40 -0400 Subject: [PATCH 19/20] cuopt service additional doc and comment tweaks --- .../utils/linear_programming/data_definition.py | 12 ++++++------ python/cuopt_server/cuopt_server/utils/solver.py | 6 ++---- python/cuopt_server/cuopt_server/webserver.py | 4 +--- 3 files changed, 9 insertions(+), 13 deletions(-) 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 2bdcb461a9..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 @@ -551,8 +551,8 @@ class SolverConfig(StrictModel): ) user_problem_file: Optional[str] = Field( default="", - description="Controls the name of a file where " - "cuOpt should write the user problem.", + description="Ignored by the service but included " + "for dataset compatibility", ) per_constraint_residual: Optional[bool] = Field( default=False, @@ -580,13 +580,13 @@ class SolverConfig(StrictModel): ) log_file: Optional[str] = Field( default="", - description="Controls the name of a log file where cuOpt " - "should write information about the solve.", + description="Ignored by the service but included " + "for dataset compatibility", ) solution_file: Optional[str] = Field( default="", - description="Controls the name of a file where " - "cuOpt should write the solution.", + description="Ignored by the service but included " + "for dataset compatibility", ) solver_mode: Optional[int] = Field( default=None, diff --git a/python/cuopt_server/cuopt_server/utils/solver.py b/python/cuopt_server/cuopt_server/utils/solver.py index 863442f2ae..15a137527d 100644 --- a/python/cuopt_server/cuopt_server/utils/solver.py +++ b/python/cuopt_server/cuopt_server/utils/solver.py @@ -98,15 +98,13 @@ def solve_LP_sync( logging.debug(f"etl_time {etl_end_time - begin_time}") if not validation_only: + # log_file setting is ignored in the service, + # instead we control it and use it as the basis for callbacks if 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}") - # We have to potentially overwrite the user log_file - # value here since we are running in the service and using - # web callbacks. The user can not use callbacks if they - # want explicit control over the log file. LP_data.solver_config.log_file = log_file elif LP_data.solver_config.log_file: warnings.append( diff --git a/python/cuopt_server/cuopt_server/webserver.py b/python/cuopt_server/cuopt_server/webserver.py index ade92463db..ac389652a6 100644 --- a/python/cuopt_server/cuopt_server/webserver.py +++ b/python/cuopt_server/cuopt_server/webserver.py @@ -958,9 +958,7 @@ async def postrequest( ), solver_logs: Optional[bool] = Query( default=False, - description="If set to True, math optimization problems will produce detailed solver logs that can be retrieved from /cuopt/log/{id}. " # noqa - "The solver_config.log_file value will be set to log_{id} overriding any user value set. If a user wants logs generated " # noqa - "on the server with a user-defined name, then do not use the callback mechanism and simply set solver_config.log_file as desired.", # 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, From 03966cfa92a3fb7cfe3af1768c77cc2f16e70863 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Tue, 29 Jul 2025 14:43:23 -0400 Subject: [PATCH 20/20] cuopt service fix disable of log_file in batch LP mode --- python/cuopt_server/cuopt_server/utils/solver.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/python/cuopt_server/cuopt_server/utils/solver.py b/python/cuopt_server/cuopt_server/utils/solver.py index 15a137527d..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: @@ -100,7 +100,13 @@ def solve_LP_sync( if not validation_only: # log_file setting is ignored in the service, # instead we control it and use it as the basis for callbacks - if solver_logging: + 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)