Skip to content

Recursive subMIP#1482

Draft
nguidotti wants to merge 28 commits into
NVIDIA:mainfrom
nguidotti:recursive-submip
Draft

Recursive subMIP#1482
nguidotti wants to merge 28 commits into
NVIDIA:mainfrom
nguidotti:recursive-submip

Conversation

@nguidotti

Copy link
Copy Markdown
Contributor

This PR implements the recursive subMIP from HiGHS.

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

@nguidotti nguidotti added this to the 26.08 milestone Jun 26, 2026
@nguidotti nguidotti added non-breaking Introduces a non-breaking change improvement Improves an existing functionality mip labels Jun 26, 2026
@copy-pr-bot

copy-pr-bot Bot commented Jun 26, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
nguidotti added 23 commits June 29, 2026 12:56
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
…bmip (and restarted) problems. Papilo can now be applied to an user_problem_t inplace.

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
# Conflicts:
#	cpp/src/mip_heuristics/diversity/lns/rins.cu
#	cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
…e old warm start code.

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
…/root change diving procedure when the RINS neighbourhood is not large enough

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
…ation count for the root relaxation (it should reflect the number of simplex iterations).

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
…. this enable the use of diving heuristics for RINS.

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
…f the neighbourhood is too loose or it already found an improving solution.

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
@nguidotti

Copy link
Copy Markdown
Contributor Author

@CodeRabbit full review

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds Sub-MIP and RINS support to branch-and-bound, including a new presolver wrapper and PaPILO-based crush/uncrush implementations, pseudo-cost warm starting, worker pool restructuring, and renamed iteration accounting (LP iterations to simplex iterations). It also adjusts initial-solution handling, adds tests/datasets, and includes minor unrelated fixes.

Changes

Sub-MIP/RINS and Presolve Crush Support

Layer / File(s) Summary
Sub-MIP settings, constants, and renames
cpp/src/dual_simplex/simplex_solver_settings.hpp, cpp/src/branch_and_bound/constants.hpp, cpp/src/branch_and_bound/deterministic_workers.hpp, cpp/src/branch_and_bound/worker.hpp, cpp/src/cuts/cuts.cpp, cpp/src/mip_heuristics/diversity/lns/rins.cu, cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh, cpp/src/branch_and_bound/pseudo_costs.cpp, cpp/src/branch_and_bound/branch_and_bound.cpp
Adds submip_settings_t, bnb_iteration_limit, mip_status_t, SUBMIP strategy, renames total_lp_iterstotal_simplex_iters, and sub_mipinside_submip.
Presolver wrapper and CMake
cpp/src/branch_and_bound/presolve.{hpp,cpp}, cpp/src/branch_and_bound/CMakeLists.txt, cpp/tests/mip/presolve_test.cu
New presolver_t delegates apply/crush/uncrush to third_party_presolve_t; adds a full-reduction test on ex9.mps.
PaPILO crush/uncrush and problem building
cpp/src/mip_heuristics/presolve/third_party_presolve.{hpp,cpp}, cpp/tests/linear_programming/unit_tests/presolve_test.cu
Adds build_papilo_problem/build_user_problem for user_problem_t, third_party_presolve_t::apply for sub-MIPs, and crush_primal_solution/crush_primal_dual_solution with KKT round-trip and warmstart tests.
Inverse simplex-to-user-problem conversion
cpp/src/dual_simplex/presolve.{hpp,cpp}, cpp/tests/dual_simplex/unit_tests/solve.cpp
Adds convert_simplex_problem reconstructing range-form problems and a round-trip test.
Pseudo-cost warm starting
cpp/src/branch_and_bound/pseudo_costs.{hpp,cpp}, cpp/src/branch_and_bound/branch_and_bound.{hpp,cpp}
Adds warm_start/warm_start_from to seed pseudo-costs from a parent problem and guards against invalid branch variables.
Diving/Sub-MIP worker pool
cpp/src/branch_and_bound/worker.hpp, cpp/src/branch_and_bound/diving_heuristics.hpp
Replaces per-strategy arrays with scalar diving-worker counters, adds submip_stats_t, skip_set_bounds, and get_diving_heuristic_list.
Branch-and-bound Sub-MIP/RINS pipeline
cpp/src/branch_and_bound/branch_and_bound.{hpp,cpp}, cpp/src/branch_and_bound/mip_node.hpp
Adds solve_submip, rins, launch_submip_worker, set_solution_from_submip, updates report_heuristic, and switches accounting to simplex iterations.
Diversity manager and early incumbent pooling
cpp/src/mip_heuristics/diversity/diversity_manager.cu, cpp/src/mip_heuristics/solve.cu, cpp/tests/mip/incumbent_callback_test.cu, datasets/mip/download_miplib_test_dataset.sh
Crushes initial solutions via PaPILO, pools multiple early heuristic incumbents for reinjection, adds tests, and expands MIPLIB dataset list.

Estimated code review effort: 5 (Critical) | ~120 minutes

Unrelated Standalone Fixes

Layer / File(s) Summary
Type qualification and lambda capture fixes
cpp/include/cuopt/mathematical_optimization/cpu_optimization_problem.hpp, cpp/src/barrier/barrier.cu
Fixes a return-type qualification and removes an unused explicit lambda capture.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Possibly related PRs

  • NVIDIA/cuopt#1104: Adds the new presolver_t crush/uncrush wrapper that depends on third_party_presolve_t crush APIs also introduced in this PR.

Suggested labels: P2

Suggested reviewers: chris-maes, tmckayus

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and directly matches the main change: recursive subMIP support.
Description check ✅ Passed The description is directly related and correctly summarizes the recursive subMIP work.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

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

⚠️ Outside diff range comments (1)
cpp/src/dual_simplex/simplex_solver_settings.hpp (1)

69-73: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

bnb_iteration_limit default uses 32-bit i_t max instead of its own int64_t width.

bnb_iteration_limit is declared int64_t but initialized with std::numeric_limits<i_t>::max() (i.e. int max ≈ 2.1B) rather than std::numeric_limits<int64_t>::max(). This silently caps the "no limit" sentinel far below what the 64-bit field can represent, unlike iteration_limit above it (which is correctly typed i_t). For very long-running/deeply recursive sub-MIP solves accumulating simplex iterations, this could trigger a premature ITERATION_LIMIT status well before an actual user-configured limit would be reached.

🔧 Proposed fix
-      bnb_iteration_limit(std::numeric_limits<i_t>::max()),
+      bnb_iteration_limit(std::numeric_limits<int64_t>::max()),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/dual_simplex/simplex_solver_settings.hpp` around lines 69 - 73, The
default initialization for bnb_iteration_limit in simplex_solver_settings should
use the full 64-bit sentinel instead of i_t max, since the member is int64_t and
should match its declared width. Update the constructor/initializer in
simplex_solver_settings so bnb_iteration_limit is initialized with the int64_t
maximum value, keeping it consistent with the other limit fields and avoiding an
unintended 32-bit cap.
🧹 Nitpick comments (2)
cpp/src/branch_and_bound/pseudo_costs.hpp (1)

130-141: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

operator= doesn't propagate the warm_start flag, so copies of a warm-started object silently lose their data on the next resize().

operator= copies pseudo_cost_sum_*/pseudo_cost_num_* but not warm_start. Any copy of a warm-started pseudo_costs_t (e.g. via the copy constructor at Line 125) will have warm_start == false, so a later resize() call will zero out the data that was just copied.

♻️ Proposed fix
   pseudo_costs_t& operator=(const pseudo_costs_t& other)
   {
     if (this != &other) {
       this->AT                   = other.AT;
       this->pdlp_warm_cache      = other.pdlp_warm_cache;
       this->pseudo_cost_num_down = other.pseudo_cost_num_down;
       this->pseudo_cost_num_up   = other.pseudo_cost_num_up;
       this->pseudo_cost_sum_down = other.pseudo_cost_sum_down;
       this->pseudo_cost_sum_up   = other.pseudo_cost_sum_up;
+      this->warm_start           = other.warm_start;
     }
     return *this;
   }

Also applies to: 176-198

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/branch_and_bound/pseudo_costs.hpp` around lines 130 - 141, The copy
assignment in pseudo_costs_t::operator= is missing the warm_start state, so
copied warm-started instances lose their preserved pseudo-cost data before a
later resize() call. Update pseudo_costs_t::operator= to copy warm_start
alongside AT, pdlp_warm_cache, and the pseudo_cost_* fields, and make sure the
same state is preserved consistently in the copy-construction path that shares
this assignment behavior.
cpp/src/dual_simplex/presolve.cpp (1)

688-767: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add defensive assertions for convert_simplex_problem's implicit "one slack per row, non-dualized" precondition.

The function silently assumes new_slacks.size() == simplex_problem.num_rows (every row has exactly one slack/artificial column) and that simplex_problem was not produced via convert_user_problem's dualization branch. If either assumption is violated, rows are left at the default 'E'/rhs=0 initialization instead of failing, silently corrupting the recovered user_problem_t.

Suggested guard
   const i_t num_slacks = static_cast<i_t>(new_slacks.size());
+  assert(num_slacks == m &&
+         "convert_simplex_problem requires exactly one slack/artificial column per row");
   const i_t new_n      = n - num_slacks;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/dual_simplex/presolve.cpp` around lines 688 - 767, Add defensive
checks in convert_simplex_problem to enforce its implicit preconditions before
rebuilding user_problem_t: verify that new_slacks contains exactly one
slack/artificial column per row and that the simplex problem was not dualized.
Use the existing symbols simplex_problem.num_rows, new_slacks, and the
convert_simplex_problem setup to assert or fail fast when these assumptions are
not met, so the row_sense/rhs reconstruction cannot silently leave rows at the
default equality state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cpp/src/branch_and_bound/branch_and_bound.cpp`:
- Around line 2151-2331: The RINS neighborhood LP solves in `rins()` are not
being counted toward `exploration_stats_.total_simplex_iters`, so the global
iteration budget can be exceeded. Update the `rins()` neighborhood-fixing loop
to merge the local `rins_stats` simplex iterations back into
`exploration_stats_` (or accumulate them once after the loop), while preserving
any intra-loop checks that rely on `rins_stats.total_simplex_iters` for
`solve_node_lp`.
- Around line 2401-2409: The RINS sub-MIP setup in branch_and_bound.cpp can
compute a NaN fixrate when integer_list is empty and num_integers is 0. Add an
early guard in the logic around has_submip / num_var_fixed so the code skips
fixrate calculation and stats updates when there are no integer variables to
fix, or otherwise ensure the sub-MIP is not created in that case. Make the same
safeguard wherever this branch is duplicated so submip_stats_.save_success,
save_infeasible, and submip_get_max_fixrate never receive a 0/0-derived value.
- Around line 1743-1747: The `bnb_iteration_limit` early-exit path in
`branch_and_bound.cpp` is missing the `--exploration_stats_.nodes_being_solved;`
cleanup that the nearby `TIME_LIMIT` and `NODE_LIMIT` branches already perform.
Update the `branch_and_bound` loop’s `bnb_iteration_limit` check so it
decrements `nodes_being_solved` before `stack.push_front(node_ptr)` and `break`,
keeping the counter consistent with `node_ptr` re-queued for later work and
preventing skew in `exploration_stats_.nodes_explored +
exploration_stats_.nodes_being_solved`.
- Around line 2177-2212: The sub-MIP settings are being built from a fresh
default instance, so recursive solves ignore parent/custom values like
iteration_limit_ratio and related submip options. Seed simplex_solver_settings_t
submip_settings from settings_.submip_settings first, then override only the
recursion-specific fields in branch_and_bound.cpp (for example node_limit,
time_limit, level, enable_rins, logging, and other sub-MIP-only toggles). Make
sure the bnb_iteration_limit calculation uses the copied parent setting rather
than the defaulted child value.

In `@cpp/src/branch_and_bound/pseudo_costs.hpp`:
- Around line 176-198: The warm-start mapping in pseudo_costs_t::warm_start_from
only checks that reduced_to_original[k] is non-negative, but it can still index
past the parent pseudo-cost arrays. Add an upper-bound assertion for orig before
reading parent.pseudo_cost_num_up, parent.pseudo_cost_sum_up,
parent.pseudo_cost_num_down, and parent.pseudo_cost_sum_down, using the parent
vectors’ size as the limit. Keep the existing lower-bound assert and place the
new check in the same loop so invalid reduced_to_original entries are caught
before any out-of-bounds access.

In `@cpp/src/branch_and_bound/worker.hpp`:
- Around line 178-197: `next_diving_heuristic()` can crash when
`diving_heuristics` is empty because it performs a modulo by
`diving_heuristics.size()`. Update the worker setup path in
`calculate_max_diving_workers`, `update_diving_heuristic_list`, or the caller
that launches diving workers so `max_diving_workers` is forced to 0 whenever no
diving heuristics are enabled, and ensure `launch_diving_worker` is never
reached in that state. Also add a defensive empty-list check in
`next_diving_heuristic()` to avoid UB if it is called unexpectedly.

In `@cpp/src/mip_heuristics/diversity/diversity_manager.cu`:
- Around line 207-210: The debug log in diversity_manager.cu is using the wrong
printf specifiers for size_t values, which can misprint or trigger undefined
behavior. Update the CUOPT_LOG_DEBUG call in the Crushed initial solution
message so the size_t arguments like sol_idx and h_crushed.size() use %zu
consistently, matching the earlier logging pattern in this flow. Keep the
existing variables and message structure, just correct the format string to
match the argument types.

In `@cpp/src/mip_heuristics/presolve/third_party_presolve.cpp`:
- Around line 1294-1298: The coeff_key lambda in third_party_presolve.cpp is
doing row-major key packing in 32-bit arithmetic, which can overflow and collide
in coeff_current for larger instances. Update the key computation and the
coeff_current key type used by crush_primal_dual_solution-related tracking to a
64-bit integer so row * n_cols_original + col cannot wrap, and adjust any
related lookups/inserts to use the widened key consistently.

In `@cpp/tests/dual_simplex/unit_tests/solve.cpp`:
- Around line 412-416: The call to convert_simplex_problem in solve.cpp passes
an extra dualize_info argument that does not match the function’s 5-parameter
signature. Update the recovery path around simplex::convert_simplex_problem and
user_problem_t<int, double> recovered so it passes only simplex_problem,
var_types, settings, new_slacks, and recovered, removing the unsupported
dualize_info argument.

In `@cpp/tests/mip/incumbent_callback_test.cu`:
- Around line 39-51: scoped_env_restore_t currently restores an originally unset
environment variable as an empty string instead of removing it, which leaks
global state across tests. Update scoped_env_restore_t to track whether the
variable existed before construction, and in ~scoped_env_restore_t use
unsetenv(name_) when it was originally absent; keep using setenv only when a
previous value was captured. Refer to scoped_env_restore_t, its constructor, and
destructor to make the fix.

In `@cpp/tests/mip/presolve_test.cu`:
- Around line 93-99: The presolve test is comparing the returned value from
presolver_t::apply() against the wrong enum type. Update the EXPECT_EQ in the
test that uses presolver.apply(user_problem, settings) to assert against
mip::mip_status_t::OPTIMAL instead of
mip::third_party_presolve_status_t::OPTIMAL, keeping the rest of the
empty-reduced-problem checks unchanged.

---

Outside diff comments:
In `@cpp/src/dual_simplex/simplex_solver_settings.hpp`:
- Around line 69-73: The default initialization for bnb_iteration_limit in
simplex_solver_settings should use the full 64-bit sentinel instead of i_t max,
since the member is int64_t and should match its declared width. Update the
constructor/initializer in simplex_solver_settings so bnb_iteration_limit is
initialized with the int64_t maximum value, keeping it consistent with the other
limit fields and avoiding an unintended 32-bit cap.

---

Nitpick comments:
In `@cpp/src/branch_and_bound/pseudo_costs.hpp`:
- Around line 130-141: The copy assignment in pseudo_costs_t::operator= is
missing the warm_start state, so copied warm-started instances lose their
preserved pseudo-cost data before a later resize() call. Update
pseudo_costs_t::operator= to copy warm_start alongside AT, pdlp_warm_cache, and
the pseudo_cost_* fields, and make sure the same state is preserved consistently
in the copy-construction path that shares this assignment behavior.

In `@cpp/src/dual_simplex/presolve.cpp`:
- Around line 688-767: Add defensive checks in convert_simplex_problem to
enforce its implicit preconditions before rebuilding user_problem_t: verify that
new_slacks contains exactly one slack/artificial column per row and that the
simplex problem was not dualized. Use the existing symbols
simplex_problem.num_rows, new_slacks, and the convert_simplex_problem setup to
assert or fail fast when these assumptions are not met, so the row_sense/rhs
reconstruction cannot silently leave rows at the default equality state.
🪄 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: Enterprise

Run ID: 02762e0c-8ec9-40b4-aa15-b48536aebd5f

📥 Commits

Reviewing files that changed from the base of the PR and between 2ca56e7 and 869cc34.

📒 Files selected for processing (29)
  • cpp/include/cuopt/mathematical_optimization/cpu_optimization_problem.hpp
  • cpp/src/barrier/barrier.cu
  • cpp/src/branch_and_bound/CMakeLists.txt
  • cpp/src/branch_and_bound/branch_and_bound.cpp
  • cpp/src/branch_and_bound/branch_and_bound.hpp
  • cpp/src/branch_and_bound/constants.hpp
  • cpp/src/branch_and_bound/deterministic_workers.hpp
  • cpp/src/branch_and_bound/diving_heuristics.hpp
  • cpp/src/branch_and_bound/mip_node.hpp
  • cpp/src/branch_and_bound/presolve.cpp
  • cpp/src/branch_and_bound/presolve.hpp
  • cpp/src/branch_and_bound/pseudo_costs.cpp
  • cpp/src/branch_and_bound/pseudo_costs.hpp
  • cpp/src/branch_and_bound/worker.hpp
  • cpp/src/cuts/cuts.cpp
  • cpp/src/dual_simplex/presolve.cpp
  • cpp/src/dual_simplex/presolve.hpp
  • cpp/src/dual_simplex/simplex_solver_settings.hpp
  • cpp/src/mip_heuristics/diversity/diversity_manager.cu
  • cpp/src/mip_heuristics/diversity/lns/rins.cu
  • cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh
  • cpp/src/mip_heuristics/presolve/third_party_presolve.cpp
  • cpp/src/mip_heuristics/presolve/third_party_presolve.hpp
  • cpp/src/mip_heuristics/solve.cu
  • cpp/tests/dual_simplex/unit_tests/solve.cpp
  • cpp/tests/linear_programming/unit_tests/presolve_test.cu
  • cpp/tests/mip/incumbent_callback_test.cu
  • cpp/tests/mip/presolve_test.cu
  • datasets/mip/download_miplib_test_dataset.sh

Comment thread cpp/src/branch_and_bound/branch_and_bound.cpp
Comment thread cpp/src/branch_and_bound/branch_and_bound.cpp
Comment thread cpp/src/branch_and_bound/branch_and_bound.cpp
Comment thread cpp/src/branch_and_bound/branch_and_bound.cpp
Comment thread cpp/src/branch_and_bound/pseudo_costs.hpp
Comment thread cpp/src/mip_heuristics/diversity/diversity_manager.cu Outdated
Comment thread cpp/src/mip_heuristics/presolve/third_party_presolve.cpp Outdated
Comment thread cpp/tests/dual_simplex/unit_tests/solve.cpp
Comment on lines +39 to +51
scoped_env_restore_t(const char* env_name, const char* new_value) : name_(env_name)
{
if (const char* prev = std::getenv(env_name)) { prev_value_ = prev; }
::setenv(env_name, new_value, 1);
}
~scoped_env_restore_t() { ::setenv(name_, prev_value_.c_str(), 1); }
scoped_env_restore_t(const scoped_env_restore_t&) = delete;
scoped_env_restore_t& operator=(const scoped_env_restore_t&) = delete;

private:
const char* name_;
std::string prev_value_;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

scoped_env_restore_t leaves the variable set to "" when it was originally unset.

The constructor only captures a previous value when one exists, but the destructor unconditionally calls setenv(name_, prev_value_.c_str(), 1). If the variable was not present before, it is restored as an empty string rather than removed, mutating global process state for subsequent tests. Track presence and unsetenv in that case.

As per path instructions: "Test isolation — no leaked GPU state or global mutation across tests".

🐛 Proposed fix
   scoped_env_restore_t(const char* env_name, const char* new_value) : name_(env_name)
   {
-    if (const char* prev = std::getenv(env_name)) { prev_value_ = prev; }
+    if (const char* prev = std::getenv(env_name)) {
+      prev_value_ = prev;
+      had_value_  = true;
+    }
     ::setenv(env_name, new_value, 1);
   }
-  ~scoped_env_restore_t() { ::setenv(name_, prev_value_.c_str(), 1); }
+  ~scoped_env_restore_t()
+  {
+    if (had_value_) {
+      ::setenv(name_, prev_value_.c_str(), 1);
+    } else {
+      ::unsetenv(name_);
+    }
+  }
   scoped_env_restore_t(const scoped_env_restore_t&)            = delete;
   scoped_env_restore_t& operator=(const scoped_env_restore_t&) = delete;

  private:
   const char* name_;
   std::string prev_value_;
+  bool had_value_ = false;
 };
📝 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
scoped_env_restore_t(const char* env_name, const char* new_value) : name_(env_name)
{
if (const char* prev = std::getenv(env_name)) { prev_value_ = prev; }
::setenv(env_name, new_value, 1);
}
~scoped_env_restore_t() { ::setenv(name_, prev_value_.c_str(), 1); }
scoped_env_restore_t(const scoped_env_restore_t&) = delete;
scoped_env_restore_t& operator=(const scoped_env_restore_t&) = delete;
private:
const char* name_;
std::string prev_value_;
};
scoped_env_restore_t(const char* env_name, const char* new_value) : name_(env_name)
{
if (const char* prev = std::getenv(env_name)) {
prev_value_ = prev;
had_value_ = true;
}
::setenv(env_name, new_value, 1);
}
~scoped_env_restore_t()
{
if (had_value_) {
::setenv(name_, prev_value_.c_str(), 1);
} else {
::unsetenv(name_);
}
}
scoped_env_restore_t(const scoped_env_restore_t&) = delete;
scoped_env_restore_t& operator=(const scoped_env_restore_t&) = delete;
private:
const char* name_;
std::string prev_value_;
bool had_value_ = false;
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/tests/mip/incumbent_callback_test.cu` around lines 39 - 51,
scoped_env_restore_t currently restores an originally unset environment variable
as an empty string instead of removing it, which leaks global state across
tests. Update scoped_env_restore_t to track whether the variable existed before
construction, and in ~scoped_env_restore_t use unsetenv(name_) when it was
originally absent; keep using setenv only when a previous value was captured.
Refer to scoped_env_restore_t, its constructor, and destructor to make the fix.

Source: Path instructions

Comment thread cpp/tests/mip/presolve_test.cu
nguidotti added 4 commits July 3, 2026 11:18
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
…sible.

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
…mber of iterations when creating the RINS neighbourhood.

Signed-off-by: Nicolas L. Guidotti <nguidotti@nvidia.com>
@nguidotti

Copy link
Copy Markdown
Contributor Author

/ok to test d5f020e

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

CI Test Summary

3 failed · 10 passed · 2 skipped

wheel-tests-cuopt / 13.0.3, 3.12, arm64, rockylinux8, l4, latest-driver, latest-deps — 1 failed test
  • tests/linear_programming/test_python_API.py::test_read_write_mps_and_relaxation
wheel-tests-cuopt / 13.0.3, 3.12, amd64, ubuntu24.04, rtxpro6000, latest-driver, latest-deps — 1 failed test
  • tests/linear_programming/test_python_API.py::test_read_write_mps_and_relaxation
wheel-tests-cuopt / 13.3.0, 3.14, amd64, ubuntu26.04, rtxpro6000, latest-driver, latest-deps — 1 failed test
  • tests/linear_programming/test_python_API.py::test_read_write_mps_and_relaxation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants