Skip to content

Expose GPU heuristics tuning parameters via config files#993

Open
aliceb-nv wants to merge 16 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 16 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

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces a MIP heuristic hyperparameter subsystem: new hyperparameter type and constants, load/dump settings file support and CLI options to dump/load params, extensive solver/heuristic code updated to read these hyperparameters, plus tests validating parsing and round-trip behavior.

Changes

Cohort / File(s) Summary
Hyperparameter type & settings member
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 defaults and a heuristic_params member in MIP solver settings.
Parameter name constants
cpp/include/cuopt/linear_programming/constants.h
Adds 17 CUOPT_MIP_HYPER_HEURISTIC_* string macros for hyperparameter keys.
Parameter metadata extension
cpp/include/cuopt/linear_programming/utilities/internals.hpp
Extends parameter_info_t (and bool/string specializations) to include a const char* description constructor parameter and member.
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(); implements robust parsing/quoting, strict numeric validation, quoting helper, and registers many new hyperparameters (with descriptions).
CLI: params-file and dump flag
cpp/cuopt_cli.cpp
Adds --params-file and --dump-hyper-params CLI options, early scan to dump hyper-parameters to stdout and exit, hides hyperparameters from normal CLI help, and extends run_single_file() signature to accept optional params_file.
Diversity & recombiners
cpp/src/mip_heuristics/diversity/diversity_manager.cu, cpp/src/mip_heuristics/diversity/recombiners/recombiner.cuh
Diversity now pulls population, infeasibility weight, LP timing and related-vars limits from heuristic params; init_enabled_recombiners gains user_enabled_mask to filter 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 unique_ptr vectors; thread counts, cycle-detection length, LP time limits, and stagnation/exit constants now configurable via hyperparams.
Problem & related-vars timeout
cpp/src/mip_heuristics/problem/problem.cu, cpp/src/mip_heuristics/problem/problem.cuh
Adds related_vars_time_limit to problem_t and uses it when computing related variables instead of a hardcoded 30s.
Presolve & root-LP timing
cpp/src/mip_heuristics/solve.cu, cpp/src/mip_heuristics/solver.cu
Replaces hardcoded presolve/root-LP time heuristics with parameterized ratio/max caps from heuristic_params; presolve debug logging added.
RINS settings refactor
cpp/src/mip_heuristics/diversity/lns/rins.cu, cpp/src/mip_heuristics/diversity/lns/rins.cuh
RINS uses heuristic_params 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 fields now provided by hyperparams (time_ratio_on_init_lp, max_time_on_lp, max_solutions, initial_infeasibility_weight).
Tests & build
cpp/tests/mip/CMakeLists.txt, cpp/tests/mip/heuristics_hyper_params_test.cu
Adds HEURISTICS_HYPER_PARAMS_TEST and a comprehensive test validating dump/load, round-trip assignments, 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 accurately summarizes the main change: exposing GPU heuristics tuning parameters via config files, which is the core feature of this PR.
Description check ✅ Passed The description is related to the changeset, providing context about the configuration file format, CLI flags, and parameter exposure, matching the implementation shown in the raw_summary.

✏️ 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.

@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

@coderabbitai coderabbitai bot Mar 31, 2026

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.

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.

rins.cuh did not hardcode 10. The time limit was reset to the default value in the constructor, which is 3. Can you check this?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the current state of rins.cuh for any time_limit defaults
echo "=== rins.cuh content around settings struct ==="
rg -n 'time_limit\|fixrate\|default_' cpp/src/mip_heuristics/diversity/lns/rins.cuh -A2 -B2

# Check git log for the old default values in rins.cuh
echo "=== Git diff for rins.cuh ==="
git diff HEAD~1 -- cpp/src/mip_heuristics/diversity/lns/rins.cuh 2>/dev/null || git log --oneline -5

Length of output: 806


@aliceb-nv, you're absolutely right. The git diff confirms that the old default_time_limit in rins_settings_t was indeed 3.0:

-  double default_time_limit         = 3.;

The new rins_time_limit default of 3.0 in heuristics_hyper_params.hpp exactly matches the previous value — there is no behavioral change here. My original comment was incorrect. Apologies for the noise!

@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.

@aliceb-nv
Copy link
Copy Markdown
Contributor Author

/ok to test 7788f3b

@aliceb-nv
Copy link
Copy Markdown
Contributor Author

Good points :) I ended up settling on the CUOPT_MIP_HYPER_HEURISTIC prefix, to keep the MIP prefix and also ensure HYPER remains a prefix if we end up adding hyperparameters for other parts of the solver.

Yes, the parameter config file loading works with all parameters!

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: 3

🤖 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/cuopt_cli.cpp`:
- Around line 93-100: The per-device RMM setup happens in main() before
run_single_file() loads params_file, so a config-only CUOPT_NUM_GPUS in
params_file is ignored; fix by merging file and CLI settings before RMM/GPU
initialization: either pre-read params_file (call
settings.load_parameters_from_file(params_file) or a lightweight parser) in
main() to extract CUOPT_NUM_GPUS and override/merge with settings_strings, or
change run_single_file()/its caller to accept a fully merged solver_settings_t
(created by loading params_file then applying settings_strings) and use that
merged settings to drive the RMM/device setup; ensure references to params_file,
settings.load_parameters_from_file, run_single_file, main, and CUOPT_NUM_GPUS
are updated accordingly.

In `@cpp/src/math_optimization/solver_settings.cu`:
- Around line 13-16: The parsing code currently uses std::stof/std::stod and
then only performs range checks, but "nan" parses successfully and escapes those
checks; update the parser wherever std::stof or std::stod is used (the
float/double parsing sites in this file) to detect and reject NaN results by
calling std::isnan(parsedValue) immediately after parsing and treating it as an
invalid config value (log/return error/throw as the surrounding code does for
other parse failures); do this for both float and double parsing sites mentioned
(the blocks using std::stof and std::stod) so malformed "nan" strings cannot
propagate into solver settings.

In `@cpp/src/mip_heuristics/problem/problem.cuh`:
- Line 331: The three copy constructors of the Problem class in problem.cuh are
not propagating the tuned related_vars_time_limit, causing copies (e.g., via
get_problem_after_fixing_vars) to reset it to the default; update each copy
constructor initializer list to include
related_vars_time_limit(problem_.related_vars_time_limit) so the copied Problem
preserves the source's related_vars_time_limit value.
🪄 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: 4f104d6c-54e1-4ffe-a4c1-5e3c5f0c2416

📥 Commits

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

📒 Files selected for processing (9)
  • cpp/cuopt_cli.cpp
  • cpp/include/cuopt/linear_programming/constants.h
  • cpp/include/cuopt/linear_programming/utilities/internals.hpp
  • cpp/src/math_optimization/solver_settings.cu
  • 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/heuristics_hyper_params_test.cu
✅ Files skipped from review due to trivial changes (3)
  • cpp/src/mip_heuristics/problem/problem.cu
  • cpp/tests/mip/heuristics_hyper_params_test.cu
  • cpp/include/cuopt/linear_programming/constants.h
🚧 Files skipped from review as they are similar to previous changes (1)
  • cpp/src/mip_heuristics/solve.cu

Comment on lines 93 to 100
const std::map<std::string, std::string>& settings_strings,
const std::string& params_file = "")
{
cuopt::linear_programming::solver_settings_t<int, double> settings;

try {
if (!params_file.empty()) { settings.load_parameters_from_file(params_file); }
for (auto& [key, val] : settings_strings) {
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 | 🟠 Major

--params-file is loaded too late for CUOPT_NUM_GPUS.

main() configures per-device RMM resources before run_single_file() loads the file, so a config-only num_gpus setting is ignored during GPU resource setup. Please merge file + CLI settings before that initialization step, or pre-read at least CUOPT_NUM_GPUS from the file.

Also applies to: 419-422

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

In `@cpp/cuopt_cli.cpp` around lines 93 - 100, The per-device RMM setup happens in
main() before run_single_file() loads params_file, so a config-only
CUOPT_NUM_GPUS in params_file is ignored; fix by merging file and CLI settings
before RMM/GPU initialization: either pre-read params_file (call
settings.load_parameters_from_file(params_file) or a lightweight parser) in
main() to extract CUOPT_NUM_GPUS and override/merge with settings_strings, or
change run_single_file()/its caller to accept a fully merged solver_settings_t
(created by loading params_file then applying settings_strings) and use that
merged settings to drive the RMM/device setup; ensure references to params_file,
settings.load_parameters_from_file, run_single_file, main, and CUOPT_NUM_GPUS
are updated accordingly.

@aliceb-nv
Copy link
Copy Markdown
Contributor Author

/ok to test 41a32f3

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