Skip to content

Expose GPU heuristics tuning parameters via config files#993

Open
aliceb-nv wants to merge 8 commits intoNVIDIA:release/26.04from
aliceb-nv:heuristics-basic-tuning
Open

Expose GPU heuristics tuning parameters via config files#993
aliceb-nv wants to merge 8 commits intoNVIDIA:release/26.04from
aliceb-nv:heuristics-basic-tuning

Conversation

@aliceb-nv
Copy link
Contributor

@aliceb-nv aliceb-nv commented Mar 24, 2026

This PR exposes some GPU heuristics tuning parameters to the user through a configuration file of the following format:

# cuOpt parameter configuration (auto-generated)
# Uncomment and change only the values you want to override.

# max solutions in pool (int, range: [1, 2147483647])
# hyper_population_size = 32

# parallel CPU FJ climbers (int, range: [0, 2147483647])
# hyper_num_cpufj_threads = 8

# FP loops w/o improvement before recombination (int, range: [1, 2147483647])
# hyper_stagnation_trigger = 3

# diversity step depth after stagnation (int, range: [1, 2147483647])
# hyper_max_iterations_without_improvement = 8

# FJ baseline local-minima exit threshold (int, range: [1, 2147483647])
# hyper_n_of_minimums_for_exit = 7000

# bitmask: 1=BP 2=FP 4=LS 8=SubMIP (int, range: [0, 15])
# hyper_enabled_recombiners = 15

# FP assignment cycle ring buffer length (int, range: [1, 2147483647])
# hyper_cycle_detection_length = 30


# ...

Parameters are only exposed through the cuopt_cli runner for the moment, via the flags --dump-mip-heuristic-config (to export a sample config file with the default parameters), and --mip-heuristic-config (to load the parameters from a file).

Description

Issue

Checklist

  • I am familiar with the Contributing Guidelines.
  • Testing
    • New or existing tests cover these changes
    • Added tests
    • Created an issue to follow-up
    • NA
  • Documentation
    • The documentation is up to date with these changes
    • Added new documentation
    • NA

@aliceb-nv aliceb-nv added this to the 26.04 milestone Mar 24, 2026
@aliceb-nv aliceb-nv requested review from a team as code owners March 24, 2026 14:57
@aliceb-nv aliceb-nv requested a review from rgsl888prabhu March 24, 2026 14:57
@aliceb-nv aliceb-nv added non-breaking Introduces a non-breaking change improvement Improves an existing functionality labels Mar 24, 2026
@aliceb-nv aliceb-nv requested review from kaatish and rg20 March 24, 2026 14:57
@copy-pr-bot
Copy link

copy-pr-bot bot commented Mar 24, 2026

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@aliceb-nv
Copy link
Contributor Author

/ok to test cc1b185

@coderabbitai
Copy link

coderabbitai bot commented Mar 24, 2026

📝 Walkthrough

Walkthrough

Adds a configurable MIP heuristic hyperparameter subsystem: new hyperparameter struct and constants, file load/dump APIs, CLI flags to show/load params, and multiple solver/heuristic components updated to consume these parameters instead of fixed constants.

Changes

Cohort / File(s) Summary
Hyperparameter Type & Integration
cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp, cpp/include/cuopt/linear_programming/mip/solver_settings.hpp
Adds mip_heuristics_hyper_params_t with defaulted tuning fields and embeds it as heuristic_params in mip_solver_settings_t.
Parameter Names & Metadata
cpp/include/cuopt/linear_programming/constants.h, cpp/include/cuopt/linear_programming/utilities/internals.hpp
Introduces string macros for hyperparameter keys and extends parameter_info_t (and specializations) with is_hyperparameter and description metadata.
Settings API: Load/Dump & Registration
cpp/include/cuopt/linear_programming/solver_settings.hpp, cpp/src/math_optimization/solver_settings.cu
Adds load_parameters_from_file() and dump_parameters_to_file(); registers many MIP hyperparameters (hidden) and implements robust parsing/serialization with quoting, comments, and strict numeric validation.
CLI: params-file & show-hyper-params
cpp/cuopt_cli.cpp
Adds --params-file and --show-hyper-params flags, early pre-parse dump-to-stdout behavior, hides hyperparameters from regular help, and extends run_single_file() signature to accept an optional params file path.
Heuristics: Config Propagation & RINS/Recombiners
cpp/src/mip_heuristics/diversity/diversity_manager.cu, cpp/src/mip_heuristics/diversity/recombiners/recombiner.cuh
Derives diversity/RINS settings from hyperparams, passes RINS time/fix settings, and adds user-enabled recombiner bitmask filtering to init_enabled_recombiners.
Local Search & Feasibility Pump
cpp/src/mip_heuristics/local_search/local_search.cu, cpp/src/mip_heuristics/local_search/local_search.cuh, cpp/src/mip_heuristics/local_search/feasibility_pump/feasibility_pump.cu, cpp/src/mip_heuristics/local_search/feasibility_pump/feasibility_pump.cuh
Replaces fixed-size CPU-FJ arrays with dynamic vectors of unique_ptrs; makes CPU thread counts, cycle-detection length, LP time limits, and stagnation/exit constants configurable from hyperparameters.
Problem & Presolve Time Limits
cpp/src/mip_heuristics/problem/problem.cu, cpp/src/mip_heuristics/problem/problem.cuh, cpp/src/mip_heuristics/solve.cu, cpp/src/mip_heuristics/solver.cu
Adds related_vars_time_limit to problem state and replaces hardcoded presolve/root LP time limits with parameterized ratios and max caps from hyperparameters; propagates related-vars timeout into relevant stages.
Recombiner Initialization API Change
cpp/src/mip_heuristics/diversity/recombiners/recombiner.cuh
Function signature updated to accept optional user_enabled_mask to selectively enable recombiners by bitmask.
Tests & Build
cpp/tests/mip/CMakeLists.txt, cpp/tests/mip/heuristics_hyper_params_test.cu
Adds a new test target and comprehensive tests for dumping/loading hyperparams, round-trip behavior, partial configs, format edge cases, and failure modes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objective of the PR: exposing GPU heuristics tuning parameters via configuration files.
Description check ✅ Passed The PR description clearly relates to the changeset, explaining the exposure of GPU heuristics tuning parameters via configuration files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
cpp/src/mip_heuristics/local_search/feasibility_pump/feasibility_pump.cuh (1)

27-28: Consider validating cycle_len parameter.

The constructor accepts cycle_len with a default of 30, which is correct. However, if cycle_len is 0 or negative:

  • curr_recent_sol would be -1 (or underflow for unsigned types)
  • The modulo operations in check_cycle (line 54) could exhibit undefined behavior for cycle_detection_length <= 0

The hyper-parameter loading should validate this, but a defensive check here would add robustness.

🛡️ Optional: Add defensive assertion
 cycle_queue_t(problem_t<i_t, f_t>& problem, i_t cycle_len = 30)
-    : cycle_detection_length(cycle_len), curr_recent_sol(cycle_detection_length - 1)
+    : cycle_detection_length(cycle_len > 0 ? cycle_len : 30), curr_recent_sol(cycle_detection_length - 1)
 {
+   cuopt_assert(cycle_len > 0, "cycle_detection_length must be positive");
    for (i_t i = 0; i < cycle_detection_length; ++i) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/mip_heuristics/local_search/feasibility_pump/feasibility_pump.cuh`
around lines 27 - 28, Validate the incoming cycle_len in the cycle_queue_t
constructor: ensure cycle_len > 0 (or enforce a minimum like 1) before assigning
it to cycle_detection_length, and adjust curr_recent_sol initialization
accordingly (e.g., set curr_recent_sol = cycle_detection_length - 1 only after
validation). Either assert/throw on invalid cycle_len or clamp it to a safe
minimum to prevent negative/underflow values and undefined behavior in
check_cycle modulo operations; update any relevant comments to reflect the
defensive check.
cpp/src/mip_heuristics/solve.cu (1)

274-276: Note: Duplicated presolve time limit calculation.

This calculation mirrors the logic in solver.cu (lines 115-116). While correct, the duplication could lead to drift if one location is updated without the other.

Consider extracting a helper function if this pattern is used elsewhere. This is not blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/mip_heuristics/solve.cu` around lines 274 - 276, The
presolve_time_limit computation is duplicated; extract it into a single helper
(e.g., getPresolveTimeLimit or computePresolveTimeLimit) that takes the
heuristic params (accessing presolve_time_ratio and presolve_max_time) and the
time_limit and returns std::min(hp.presolve_time_ratio * time_limit,
hp.presolve_max_time); replace the duplicated lines in both solve.cu (the chunk
using settings.heuristic_params and time_limit) and solver.cu with calls to this
helper so updates remain consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cpp/src/mip_heuristics/heuristics_hyper_params_loader.hpp`:
- Around line 133-134: The call to ::isspace in the std::find_if_not predicate
can invoke UB for signed char values; change the predicate passed to
std::find_if_not(line.begin(), line.end(), ::isspace) to a lambda that casts the
input char to unsigned char before calling std::isspace (e.g., use a predicate
like [](char ch){ return std::isspace(static_cast<unsigned char>(ch)); }), so
update the find_if_not call that sets first_non_ws to use this safe predicate.
- Around line 168-184: The function currently writes out params using the
ofstream variable `file` (and iterates `int_params` and `double_params` using
`params.*p.field`) but returns true without checking I/O status; after finishing
all writes (and flushing/closing `file`), check the stream state (e.g.,
`file.fail()`/`file.good()` or equivalent) and if any write error occurred log
using `CUOPT_LOG_ERROR` and return false, otherwise return true.

---

Nitpick comments:
In `@cpp/src/mip_heuristics/local_search/feasibility_pump/feasibility_pump.cuh`:
- Around line 27-28: Validate the incoming cycle_len in the cycle_queue_t
constructor: ensure cycle_len > 0 (or enforce a minimum like 1) before assigning
it to cycle_detection_length, and adjust curr_recent_sol initialization
accordingly (e.g., set curr_recent_sol = cycle_detection_length - 1 only after
validation). Either assert/throw on invalid cycle_len or clamp it to a safe
minimum to prevent negative/underflow values and undefined behavior in
check_cycle modulo operations; update any relevant comments to reflect the
defensive check.

In `@cpp/src/mip_heuristics/solve.cu`:
- Around line 274-276: The presolve_time_limit computation is duplicated;
extract it into a single helper (e.g., getPresolveTimeLimit or
computePresolveTimeLimit) that takes the heuristic params (accessing
presolve_time_ratio and presolve_max_time) and the time_limit and returns
std::min(hp.presolve_time_ratio * time_limit, hp.presolve_max_time); replace the
duplicated lines in both solve.cu (the chunk using settings.heuristic_params and
time_limit) and solver.cu with calls to this helper so updates remain
consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d9ee3863-cc97-4a20-b7d3-216ed23a9be8

📥 Commits

Reviewing files that changed from the base of the PR and between cbec0e9 and cc1b185.

📒 Files selected for processing (17)
  • benchmarks/linear_programming/cuopt/run_mip.cpp
  • cpp/cuopt_cli.cpp
  • cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp
  • cpp/include/cuopt/linear_programming/mip/solver_settings.hpp
  • cpp/src/mip_heuristics/diversity/diversity_manager.cu
  • cpp/src/mip_heuristics/diversity/recombiners/recombiner.cuh
  • cpp/src/mip_heuristics/heuristics_hyper_params_loader.hpp
  • cpp/src/mip_heuristics/local_search/feasibility_pump/feasibility_pump.cu
  • cpp/src/mip_heuristics/local_search/feasibility_pump/feasibility_pump.cuh
  • cpp/src/mip_heuristics/local_search/local_search.cu
  • cpp/src/mip_heuristics/local_search/local_search.cuh
  • cpp/src/mip_heuristics/problem/problem.cu
  • cpp/src/mip_heuristics/problem/problem.cuh
  • cpp/src/mip_heuristics/solve.cu
  • cpp/src/mip_heuristics/solver.cu
  • cpp/tests/mip/CMakeLists.txt
  • cpp/tests/mip/heuristics_hyper_params_test.cu

Comment on lines +133 to +134
auto first_non_ws = std::find_if_not(line.begin(), line.end(), ::isspace);
if (first_non_ws == line.end() || *first_non_ws == '#') continue;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential undefined behavior with ::isspace on signed chars.

::isspace(int) has undefined behavior when the argument is not in the range [0, UCHAR_MAX] or equal to EOF. Since char may be signed, characters with values ≥128 (extended ASCII, UTF-8 continuation bytes) become negative and trigger UB.

Proposed fix: cast to unsigned char
-    auto first_non_ws = std::find_if_not(line.begin(), line.end(), ::isspace);
+    auto first_non_ws = std::find_if_not(line.begin(), line.end(),
+                                         [](unsigned char c) { return std::isspace(c); });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
auto first_non_ws = std::find_if_not(line.begin(), line.end(), ::isspace);
if (first_non_ws == line.end() || *first_non_ws == '#') continue;
auto first_non_ws = std::find_if_not(line.begin(), line.end(),
[](unsigned char c) { return std::isspace(c); });
if (first_non_ws == line.end() || *first_non_ws == '#') continue;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/mip_heuristics/heuristics_hyper_params_loader.hpp` around lines 133 -
134, The call to ::isspace in the std::find_if_not predicate can invoke UB for
signed char values; change the predicate passed to
std::find_if_not(line.begin(), line.end(), ::isspace) to a lambda that casts the
input char to unsigned char before calling std::isspace (e.g., use a predicate
like [](char ch){ return std::isspace(static_cast<unsigned char>(ch)); }), so
update the find_if_not call that sets first_non_ws to use this safe predicate.

Comment on lines +168 to +184
std::ofstream file(path);
if (!file.is_open()) {
CUOPT_LOG_ERROR("Cannot open file for writing: %s", path.c_str());
return false;
}
file << "# MIP heuristic hyper-parameters (auto-generated)\n";
file << "# Uncomment and change only the values you want to override.\n\n";
for (const auto& p : int_params) {
file << "# " << p.description << " (int, range: [" << p.min_val << ", " << p.max_val << "])\n";
file << "# " << p.name << " = " << params.*p.field << "\n\n";
}
for (const auto& p : double_params) {
file << "# " << p.description << " (double, range: [" << p.min_val << ", " << p.max_val
<< "])\n";
file << "# " << p.name << " = " << params.*p.field << "\n\n";
}
return true;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing write error check before returning success.

The function returns true without verifying that all writes succeeded. If the disk fills up or another I/O error occurs during the streaming operations, the file may be incomplete but the caller believes the dump succeeded.

Proposed fix: check stream state before returning
   for (const auto& p : double_params) {
     file << "# " << p.description << " (double, range: [" << p.min_val << ", " << p.max_val
          << "])\n";
     file << "# " << p.name << " = " << params.*p.field << "\n\n";
   }
+  if (!file) {
+    CUOPT_LOG_ERROR("Write error while dumping config to: %s", path.c_str());
+    return false;
+  }
   return true;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/mip_heuristics/heuristics_hyper_params_loader.hpp` around lines 168 -
184, The function currently writes out params using the ofstream variable `file`
(and iterates `int_params` and `double_params` using `params.*p.field`) but
returns true without checking I/O status; after finishing all writes (and
flushing/closing `file`), check the stream state (e.g.,
`file.fail()`/`file.good()` or equivalent) and if any write error occurred log
using `CUOPT_LOG_ERROR` and return false, otherwise return true.

@chris-maes
Copy link
Contributor

Hey @aliceb-nv great to see so many parameters exposed for heuristics. What do you think about make these parameters available alongside the rest of settings in solver_settings.hpp? I think it's a bit strange to have two different methods of setting parameters (one through Python/C and command-line and the other via a config file).

@aliceb-nv aliceb-nv removed this from the 26.04 milestone Mar 24, 2026
@anandhkb
Copy link
Contributor

Is this a must have for 26.04 @aliceb-nv ? Or it can go after 26.04 as a fast follow?
@chris-maes for viz

@rg20
Copy link
Contributor

rg20 commented Mar 24, 2026

Hey @aliceb-nv great to see so many parameters exposed for heuristics. What do you think about make these parameters available alongside the rest of settings in solver_settings.hpp? I think it's a bit strange to have two different methods of setting parameters (one through Python/C and command-line and the other via a config file).

I think so too. Having a similar path for hyper parameter makes sense. With that, we can also extend support for config file. We would just read in the values and set the parameters, if it is convenient. We can label these parameters as something like CUOPT_HYPER_ to distinguish them from standard parameters.

@anandhkb
Copy link
Contributor

Hey @aliceb-nv great to see so many parameters exposed for heuristics. What do you think about make these parameters available alongside the rest of settings in solver_settings.hpp? I think it's a bit strange to have two different methods of setting parameters (one through Python/C and command-line and the other via a config file).

I think so too. Having a similar path for hyper parameter makes sense. With that, we can also extend support for config file. We would just read in the values and set the parameters, if it is convenient. We can label these parameters as something like CUOPT_HYPER_ to distinguish them from standard parameters.

+1 to @rg20 's naming convention

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
cpp/src/math_optimization/solver_settings.cu (1)

519-584: Config file parsing is robust with good RAII pattern.

The std::ifstream destructor handles cleanup. Error messages include line context, which aids debugging. A few observations:

  1. The quoted-string parser only handles \" escapes (addressed in the quote_if_needed comment above).
  2. Consider checking file.bad() after the loop to detect I/O errors that occurred during reading (as opposed to EOF).
Optional: add stream error check after loop
   }
+  if (file.bad()) {
+    cuopt_expects(false,
+                  error_type_t::ValidationError,
+                  "Parameter config: I/O error reading: %s",
+                  path.c_str());
+  }
   CUOPT_LOG_INFO("Parameters loaded from: %s", path.c_str());
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/math_optimization/solver_settings.cu` around lines 519 - 584, The
quoted-string parser is limited and you should also detect stream I/O errors
after reading; in solver_settings_t<i_t,f_t>::load_parameters_from_file, after
the getline loop completes add a check on the ifstream state (e.g., file.bad()
or !file.eof() combined appropriately) and raise the same ValidationError via
cuopt_expects if an I/O error occurred while reading the file so we distinguish
read failures from normal EOF; keep the existing error messaging style and
call-site (CUOPT_LOG_INFO) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cpp/src/math_optimization/solver_settings.cu`:
- Around line 586-622: The function
solver_settings_t<i_t,f_t>::dump_parameters_to_file writes to an ofstream but
always returns true; after performing the writes (after the string_parameters
loop) flush the stream (file.flush()) and check the stream state (e.g.,
file.fail() or !file.good()) and return false if the stream is in a bad state so
I/O errors are propagated; on failure log an error (use CUOPT_LOG_ERROR with the
path) and otherwise return true.
- Around line 46-60: The function quote_if_needed currently only escapes double
quotes but not backslashes, causing strings like foo\" to corrupt on round-trip;
modify quote_if_needed to also escape backslashes (turn '\' into "\\") before
escaping quotes so output uses \\ for backslashes and \" for quotes, and then
update the parser logic that unescapes sequences (the code that handles escaped
characters in the string parsing routine) to recognize and correctly handle the
two-character escape sequence '\\' in addition to '\"' so both backslashes and
quotes round-trip correctly.

---

Nitpick comments:
In `@cpp/src/math_optimization/solver_settings.cu`:
- Around line 519-584: The quoted-string parser is limited and you should also
detect stream I/O errors after reading; in
solver_settings_t<i_t,f_t>::load_parameters_from_file, after the getline loop
completes add a check on the ifstream state (e.g., file.bad() or !file.eof()
combined appropriately) and raise the same ValidationError via cuopt_expects if
an I/O error occurred while reading the file so we distinguish read failures
from normal EOF; keep the existing error messaging style and call-site
(CUOPT_LOG_INFO) unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d8b3e296-bc8d-433b-9b80-be8d95667d30

📥 Commits

Reviewing files that changed from the base of the PR and between cc1b185 and 390be0c.

📒 Files selected for processing (7)
  • cpp/cuopt_cli.cpp
  • cpp/include/cuopt/linear_programming/constants.h
  • cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp
  • cpp/include/cuopt/linear_programming/solver_settings.hpp
  • cpp/include/cuopt/linear_programming/utilities/internals.hpp
  • cpp/src/math_optimization/solver_settings.cu
  • cpp/tests/mip/heuristics_hyper_params_test.cu
✅ Files skipped from review due to trivial changes (1)
  • cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp
🚧 Files skipped from review as they are similar to previous changes (2)
  • cpp/tests/mip/heuristics_hyper_params_test.cu
  • cpp/cuopt_cli.cpp

Comment on lines +46 to +60
std::string quote_if_needed(const std::string& s)
{
bool needs_quoting = s.empty() || s.find(' ') != std::string::npos ||
s.find('"') != std::string::npos || s.find('\t') != std::string::npos;
if (!needs_quoting) return s;
std::string out = "\"";
for (char c : s) {
if (c == '"')
out += "\\\"";
else
out += c;
}
out += '"';
return out;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

quote_if_needed doesn't escape backslashes, risking round-trip corruption.

If a string value contains a literal backslash followed by a quote (e.g., foo\"), it will be serialized as "foo\"" but parsed back as foo" (the backslash is consumed as an escape). Consider escaping backslashes as well for a correct round-trip.

Proposed fix
 std::string quote_if_needed(const std::string& s)
 {
   bool needs_quoting = s.empty() || s.find(' ') != std::string::npos ||
-                       s.find('"') != std::string::npos || s.find('\t') != std::string::npos;
+                       s.find('"') != std::string::npos || s.find('\t') != std::string::npos ||
+                       s.find('\\') != std::string::npos;
   if (!needs_quoting) return s;
   std::string out = "\"";
   for (char c : s) {
-    if (c == '"')
+    if (c == '\\')
+      out += "\\\\";
+    else if (c == '"')
       out += "\\\"";
     else
       out += c;
   }
   out += '"';
   return out;
 }

And update the parser (lines 554-557) to handle \\:

       while (iss.get(ch)) {
-        if (ch == '\\' && iss.peek() == '"') {
+        if (ch == '\\') {
           iss.get(ch);
-          val += '"';
+          val += ch;  // handles both \" and \\
         } else if (ch == '"') {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/math_optimization/solver_settings.cu` around lines 46 - 60, The
function quote_if_needed currently only escapes double quotes but not
backslashes, causing strings like foo\" to corrupt on round-trip; modify
quote_if_needed to also escape backslashes (turn '\' into "\\") before escaping
quotes so output uses \\ for backslashes and \" for quotes, and then update the
parser logic that unescapes sequences (the code that handles escaped characters
in the string parsing routine) to recognize and correctly handle the
two-character escape sequence '\\' in addition to '\"' so both backslashes and
quotes round-trip correctly.

@aliceb-nv
Copy link
Contributor Author

/ok to test c08115b

@aliceb-nv
Copy link
Contributor Author

aliceb-nv commented Mar 24, 2026

You all make convincing arguments :) Agreed, the hyperparameters should indeed belong with the other params. I've modified the PR in consequence.
@anandhkb I think this could a fast-follow. Nightlies are available if any interested user wants to try this

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
cpp/src/math_optimization/solver_settings.cu (2)

532-535: Potential undefined behavior with ::isspace on signed char.

std::isspace(int) has undefined behavior when passed a value not representable as unsigned char (and not EOF). On systems where char is signed, extended ASCII characters (e.g., UTF-8 continuation bytes) can produce negative values.

Proposed fix: cast to unsigned char
-    auto first_non_ws = std::find_if_not(line.begin(), line.end(), ::isspace);
+    auto first_non_ws = std::find_if_not(line.begin(), line.end(),
+                                         [](unsigned char c) { return std::isspace(c); });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/math_optimization/solver_settings.cu` around lines 532 - 535, The
call to ::isspace inside the std::find_if_not lambda can invoke undefined
behavior for signed char values; update the predicate used with
std::find_if_not(line.begin(), line.end(), ::isspace) so it casts each character
to unsigned char before calling std::isspace (e.g., call
::isspace(static_cast<unsigned char>(c)) or use a small lambda that does this)
to ensure defined behavior when scanning lines in the code that manipulates
line.begin()/line.end().

554-563: Parser only handles \" escape, not \\.

This is related to the quote_if_needed issue. The parser consumes \ only when followed by ", but a literal backslash in the config would be passed through unchanged. If quote_if_needed is updated to escape backslashes, this parser must also be updated to handle \\.

Proposed fix: handle both escape sequences
       while (iss.get(ch)) {
-        if (ch == '\\' && iss.peek() == '"') {
+        if (ch == '\\') {
           iss.get(ch);
-          val += '"';
+          val += ch;  // handles both \" and \\
         } else if (ch == '"') {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/math_optimization/solver_settings.cu` around lines 554 - 563, The
parser loop that reads quoted strings (the while loop handling ch, as used in
solver_settings parsing) currently only treats backslash when followed by a
quote (consumes '\' then '"' and appends '"' ), so it fails to handle escape of
a literal backslash; update that loop to recognize and consume both escape
sequences: when ch == '\\' and iss.peek() == '"' append '"' (as now), and when
ch == '\\' and iss.peek() == '\\' consume the second backslash and append a
single backslash; otherwise keep existing behavior for closing quote and normal
characters. Ensure this logic is in the same function that reads quoted values
so it stays consistent with any changes to quote_if_needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cpp/src/math_optimization/solver_settings.cu`:
- Around line 532-535: The call to ::isspace inside the std::find_if_not lambda
can invoke undefined behavior for signed char values; update the predicate used
with std::find_if_not(line.begin(), line.end(), ::isspace) so it casts each
character to unsigned char before calling std::isspace (e.g., call
::isspace(static_cast<unsigned char>(c)) or use a small lambda that does this)
to ensure defined behavior when scanning lines in the code that manipulates
line.begin()/line.end().
- Around line 554-563: The parser loop that reads quoted strings (the while loop
handling ch, as used in solver_settings parsing) currently only treats backslash
when followed by a quote (consumes '\' then '"' and appends '"' ), so it fails
to handle escape of a literal backslash; update that loop to recognize and
consume both escape sequences: when ch == '\\' and iss.peek() == '"' append '"'
(as now), and when ch == '\\' and iss.peek() == '\\' consume the second
backslash and append a single backslash; otherwise keep existing behavior for
closing quote and normal characters. Ensure this logic is in the same function
that reads quoted values so it stays consistent with any changes to
quote_if_needed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8752473e-b1c5-4f83-b9dc-29546c2ceb84

📥 Commits

Reviewing files that changed from the base of the PR and between 390be0c and c08115b.

📒 Files selected for processing (3)
  • cpp/include/cuopt/linear_programming/constants.h
  • cpp/src/math_optimization/solver_settings.cu
  • cpp/tests/mip/heuristics_hyper_params_test.cu
✅ Files skipped from review due to trivial changes (2)
  • cpp/tests/mip/heuristics_hyper_params_test.cu
  • cpp/include/cuopt/linear_programming/constants.h

@mlubin
Copy link
Contributor

mlubin commented Mar 24, 2026

Agreed with @chris-maes that ideally all parameters should go through the same code path. Otherwise it's messy for all upstream interfaces.

Copy link
Contributor

@rg20 rg20 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great initiative! I have some minor comments.

T max_value;
T default_value;
bool is_hyperparameter;
const char* description;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about adding the description for the purpose of cuopt_cli --help, however, I decided against it in favor of making the names themselves self explanatory. Further more the the macros are defined as strings, we can probably use them as descriptions. Wdyt?

#define CUOPT_PDLP_PRECISION "pdlp_precision"

/* @brief MIP heuristic hyper-parameter names */
#define CUOPT_HYPER_POPULATION_SIZE "hyper_population_size"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these strings are not directly used anywhere, so you can make these as descriptive as you want and use them for descriptions.

T min,
T max,
T def,
bool is_hyperparameter = false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we need is_hyperparameter are you using this anywhere? we have HYPER in the parameter names, I think we can deduce this.

diversity_config_t make_diversity_config(const mip_heuristics_hyper_params_t& hp)
{
diversity_config_t c;
c.max_solutions = hp.population_size;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel this is error prone. Now, you will have to add new parameters at two places. How about compose the mip_heuristics_hyper_params_t class with objects like diversity_config_t and rins_settings_t directly?

@chris-maes chris-maes added the P1 label Mar 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Improves an existing functionality non-breaking Introduces a non-breaking change P1

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants