Skip to content

Expose GPU heuristics tuning parameters via config files#993

Open
aliceb-nv wants to merge 10 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 10 commits intoNVIDIA:release/26.04from
aliceb-nv:heuristics-basic-tuning

Conversation

@aliceb-nv
Copy link
Copy Markdown
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 Markdown

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
Copy Markdown
Contributor Author

/ok to test cc1b185

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

📝 Walkthrough

Walkthrough

Adds a MIP heuristic hyperparameter subsystem: new hyperparameter type and string constants, load/dump settings file support, CLI flags to show/load params, tests, and many solver/heuristic components updated to consume these configurable hyperparameters instead of hard-coded 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
New mip_heuristics_hyper_params_t struct with defaults; added heuristic_params member to MIP solver settings.
Parameter names & metadata
cpp/include/cuopt/linear_programming/constants.h, cpp/include/cuopt/linear_programming/utilities/internals.hpp
Adds 17 hyper_* macro strings and extends parameter_info_t (and specializations) with is_hyperparameter and description metadata.
Settings API, parsing & 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(); robust parsing/quoting, strict numeric validation, and registration of many hyperparameters (marked hidden).
CLI: params-file & show-hyper-params
cpp/cuopt_cli.cpp
Adds --params-file and --show-hyper-params, early pre-parse to dump hyperparams to stdout, hides hyperparameters from normal CLI output, and extends run_single_file() signature to accept an optional params file.
Heuristics propagation & recombiners
cpp/src/mip_heuristics/diversity/diversity_manager.cu, cpp/src/mip_heuristics/diversity/recombiners/recombiner.cuh
Diversity and RINS now read hyperparams for population/weights/time limits; init_enabled_recombiners gains optional user_enabled_mask to filter 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
Replaced fixed-size CPU-FJ arrays with dynamically sized unique_ptr vectors; made thread counts, cycle-detection length, LP time limits, stagnation/exit constants configurable from hyperparams.
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
Added related_vars_time_limit to problem state and replaced hardcoded presolve/root-LP time limits with parameterized ratio/max caps sourced from hyperparams; propagated related-vars timeout.
RINS settings refactor
cpp/src/mip_heuristics/diversity/lns/rins.cu, cpp/src/mip_heuristics/diversity/lns/rins.cuh
RINS now uses hyperparams for fixrate and time limits; removed old default/max/default time-limit fields from rins_settings_t.
Diversity config cleanup
cpp/src/mip_heuristics/diversity/diversity_config.hpp
Removed several fields (time_ratio_on_init_lp, max_time_on_lp, max_solutions, initial_infeasibility_weight) now sourced from hyperparams.
Tests & build
cpp/tests/mip/CMakeLists.txt, cpp/tests/mip/heuristics_hyper_params_test.cu
Adds HEURISTICS_HYPER_PARAMS_TEST and a comprehensive test suite validating dump/load, round-trip, parsing edge cases, and error conditions.

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 clearly and concisely summarizes the main change: exposing GPU heuristics tuning parameters via configuration files.
Description check ✅ Passed The description explains the feature (exposing tuning parameters through config files), provides a format example, and mentions CLI flags (--dump-mip-heuristic-config and --mip-heuristic-config) for the functionality.

✏️ 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
Copy Markdown

@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
Copy Markdown

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
Copy Markdown

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown

@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
Copy Markdown

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
Copy Markdown
Contributor Author

/ok to test c08115b

@aliceb-nv
Copy link
Copy Markdown
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
Copy Markdown

@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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I see your point :) I was however a bit worried about the UX - some of these hyperparameters are named after internal implementation details which aren't immediately obvious or clear; this is why I thought a description field could be relevant to help guide tuning efforts (e.g. what results to expect from this "knob")

#define CUOPT_PDLP_PRECISION "pdlp_precision"

/* @brief MIP heuristic hyper-parameter names */
#define CUOPT_HYPER_POPULATION_SIZE "hyper_population_size"
Copy link
Copy Markdown
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
Copy Markdown
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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I use it to hide these by defaults in the cli --help to avoid cluttering. True that it could be deduced from the HYPER_ prefix :) Do you think it'd be better to do this by looking at the prefix?

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
Copy Markdown
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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Much cleaner this way, agreed. Will fix

@chris-maes chris-maes added the P1 label Mar 25, 2026
@aliceb-nv aliceb-nv added this to the 26.04 milestone Mar 31, 2026
@aliceb-nv
Copy link
Copy Markdown
Contributor Author

/ok to test 42f977d

Copy link
Copy Markdown

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cpp/include/cuopt/linear_programming/mip/solver_settings.hpp (1)

10-20: ⚠️ Potential issue | 🟡 Minor

Duplicate #include <vector> detected.

<vector> is included at both line 10 and line 20. Remove one occurrence.

Proposed fix
 `#include` <vector>
 
 `#include` <cuopt/linear_programming/constants.h>
 `#include` <cuopt/linear_programming/mip/heuristics_hyper_params.hpp>
 `#include` <cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh>
 `#include` <cuopt/linear_programming/utilities/internals.hpp>
 
 `#include` <raft/core/device_span.hpp>
 `#include` <rmm/device_uvector.hpp>
-
-#include <vector>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/include/cuopt/linear_programming/mip/solver_settings.hpp` around lines 10
- 20, Remove the duplicate include of the standard header by deleting one of the
redundant "#include <vector>" lines in the top of the header
(cuopt/linear_programming/mip/solver_settings.hpp); ensure only a single
"#include <vector>" remains so the included symbols used by the file (e.g.,
anywhere using std::vector) are still available.
🤖 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/diversity/lns/rins.cu`:
- Around line 34-35: The change makes time_limit (variable time_limit in
rins.cu) derive from context.settings.heuristic_params.rins_time_limit which
defaults to 3.0 in heuristics_hyper_params.hpp, reducing the RINS sub-MIP time
limit from the previous hardcoded 10.0s; either explicitly document this tuning
decision in the code/comments (e.g., in rins.cu and heuristics_hyper_params.hpp)
stating why 3.0s is chosen, or revert the default to preserve prior behavior by
setting rins_time_limit default back to 10.0 in heuristics_hyper_params.hpp (and
ensure any comment in rins.cu reflects the chosen default) so existing behavior
remains unchanged.

---

Outside diff comments:
In `@cpp/include/cuopt/linear_programming/mip/solver_settings.hpp`:
- Around line 10-20: Remove the duplicate include of the standard header by
deleting one of the redundant "#include <vector>" lines in the top of the header
(cuopt/linear_programming/mip/solver_settings.hpp); ensure only a single
"#include <vector>" remains so the included symbols used by the file (e.g.,
anywhere using std::vector) are still available.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8cbf2856-6a24-4436-9839-378438e5eb69

📥 Commits

Reviewing files that changed from the base of the PR and between c08115b and 42f977d.

📒 Files selected for processing (9)
  • cpp/cuopt_cli.cpp
  • cpp/include/cuopt/linear_programming/constants.h
  • cpp/include/cuopt/linear_programming/mip/solver_settings.hpp
  • cpp/src/math_optimization/solver_settings.cu
  • cpp/src/mip_heuristics/diversity/diversity_config.hpp
  • cpp/src/mip_heuristics/diversity/diversity_manager.cu
  • cpp/src/mip_heuristics/diversity/lns/rins.cu
  • cpp/src/mip_heuristics/diversity/lns/rins.cuh
  • cpp/src/mip_heuristics/solve.cu
💤 Files with no reviewable changes (1)
  • cpp/src/mip_heuristics/diversity/lns/rins.cuh
✅ Files skipped from review due to trivial changes (1)
  • cpp/src/mip_heuristics/solve.cu
🚧 Files skipped from review as they are similar to previous changes (3)
  • cpp/cuopt_cli.cpp
  • cpp/src/mip_heuristics/diversity/diversity_manager.cu
  • cpp/src/math_optimization/solver_settings.cu

Comment on lines +34 to +35
fixrate = context.settings.heuristic_params.rins_fix_rate;
time_limit = context.settings.heuristic_params.rins_time_limit;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the old default value and check for any documentation of this change

# Check the old default in rins.cuh
echo "=== Old default in rins.cuh ==="
rg -n 'time_limit\{' cpp/src/mip_heuristics/diversity/lns/rins.cuh

# Check the new default in heuristics_hyper_params.hpp
echo "=== New default in heuristics_hyper_params.hpp ==="
rg -n 'rins_time_limit' cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp

# Check for any changelog or migration notes
echo "=== Changelog/migration notes ==="
fd -t f -i 'changelog\|migration\|release' -x head -50 {} \;

Repository: NVIDIA/cuopt

Length of output: 283


Confirm intentional default change: time_limit reduced from 10.0 to 3.0 seconds.

Lines 34-35 now initialize time_limit from context.settings.heuristic_params.rins_time_limit, which defaults to 3.0 (per heuristics_hyper_params.hpp). Previously, rins.cuh hardcoded time_limit{10.}. This 70% reduction in the RINS sub-MIP time limit is a significant behavioral change with no documented justification.

Either document this as an intentional tuning decision, or align the new default to the previous value of 10.0 to preserve existing behavior.

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

In `@cpp/src/mip_heuristics/diversity/lns/rins.cu` around lines 34 - 35, The
change makes time_limit (variable time_limit in rins.cu) derive from
context.settings.heuristic_params.rins_time_limit which defaults to 3.0 in
heuristics_hyper_params.hpp, reducing the RINS sub-MIP time limit from the
previous hardcoded 10.0s; either explicitly document this tuning decision in the
code/comments (e.g., in rins.cu and heuristics_hyper_params.hpp) stating why
3.0s is chosen, or revert the default to preserve prior behavior by setting
rins_time_limit default back to 10.0 in heuristics_hyper_params.hpp (and ensure
any comment in rins.cu reflects the chosen default) so existing behavior remains
unchanged.


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

Choose a reason for hiding this comment

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

Maybe call these CUOPT_HEURISTIC_***

@chris-maes
Copy link
Copy Markdown
Contributor

Thinking a bit about naming (there are only two hard things in computer science: naming and cache invalidation).

We've tried to use the prefix "mip" for all MIP setting names. What about calling these ```

#define CUOPT_MIP_HEURISTIC_HYPER_XXX "mip_heurisitic_hyper_xxx"

if that is too wordy you could also do

#define CUOPT_MIP_HYPER_XXX "mip_hyper_xxx"

We could use the convention that if a parameter has the name "hyper" in it we don't print it out in cuopt_cli and it is undocumented. (Although, I think it would be ok to just expose these all as regular parameters and just deprecate them/change them later).

Also, does your settings file work for all settings? Not just the hyper ones? If so, that is a useful feature.

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