Skip to content

GPU heuristics determinism integration#8

Closed
aliceb-nv wants to merge 302 commits into
mainfrom
gpudet
Closed

GPU heuristics determinism integration#8
aliceb-nv wants to merge 302 commits into
mainfrom
gpudet

Conversation

@aliceb-nv
Copy link
Copy Markdown
Owner

Dummy PR to trigger coderabbit review

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 8, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds deterministic execution and work-unit accounting across the MIP stack: introduces bitmask determinism flags, extended incumbent callbacks with origin/work-timestamp, a termination-checker and work-limit context, deterministic ordering/instrumentation, and widespread API and implementation changes across solver settings, B&B, GPU heuristics, local search, presolve, utilities, and tests.

Changes

Cohort / File(s) Summary
Determinism flags & public C API
cpp/include/cuopt/linear_programming/constants.h, cpp/include/cuopt/linear_programming/cuopt_c.h, cpp/src/pdlp/cuopt_c.cpp
Introduce bitset determinism macros and MIP solution origin constants; add cuOptMIPSolutionCallbackInfo, extended callback type cuOptMIPGetSolutionCallbackExt, and cuOptSetMIPGetSolutionCallbackExt + C++ wrapper.
Solver settings & context
cpp/include/.../mip/solver_settings.hpp, cpp/src/math_optimization/solver_settings.cu, cpp/src/mip_heuristics/solver_context.cuh, cpp/src/mip_heuristics/solver.cuh, cpp/src/mip_heuristics/solver.cu
Add work-unit scaling fields, gpu_heur_wait_for_exploration, change determinism_mode semantics/default to bitmask, wire solution_publication/injection, and pass termination checker into solver/context.
Incumbent callbacks & publication
cpp/include/.../utilities/internals.hpp, cpp/src/mip_heuristics/solution_callbacks.cuh, cpp/src/mip_heuristics/solve.cu, cpp/src/mip_heuristics/solver.cu
Add extended get-solution callback interface and callback-info, centralize payload build/publish/dispatch/injection, replace ad-hoc callback calls with new infrastructure.
Branch-and-Bound & replay
cpp/src/branch_and_bound/branch_and_bound.cpp, .../branch_and_bound.hpp, .../deterministic_workers.hpp
Propagate solution provenance (origin/work_timestamp) through B&B APIs, add emit/retire helpers, deterministic replay/queueing, worker snapshots/statistics, and exploration synchronization APIs.
Work/termination infra
cpp/src/utilities/termination_checker.hpp, .../work_limit_context.hpp, .../work_unit_scheduler.*, cpp/src/utilities/timer.hpp
Add termination_checker_t supporting wall-time and deterministic work-limits, extend work_limit_context with producer sync/scaling, add scheduler base offset, and add source-location params to timer checks.
Relaxed LP / dual-simplex / pseudo-costs
cpp/src/mip_heuristics/relaxed_lp/*, cpp/src/dual_simplex/*, cpp/src/branch_and_bound/pseudo_costs.*, .../pseudo_costs.hpp
Thread work_limit/work_context into LP solves and strong-branching; add iteration/work limits, deterministic work recording, reliability-selection refactor, and guarded numeric/log terms.
GPU heuristics, diversity & recombiners
cpp/src/mip_heuristics/diversity/..., .../recombiners/*, .../recombiner_stats.hpp, .../multi_armed_bandit.cuh, .../weights.cuh
Switch timers→termination_checker, add determinism instrumentation, recombiners now return (solution,bool,work), track recombiner work and use determinism-aware normalization, and enablement filtering based on context.
Feasibility-jump & kernels
cpp/src/mip_heuristics/feasibility_jump/*, .../feasibility_jump_kernels.*, .../utils.cuh
Add deterministic work estimator and accounting, deterministic tie-breaking/sorting, contiguous_set sort, kernel renames/adjustments, and many timer→termination_checker conversions.
Local search & rounding / constraint propagation
cpp/src/mip_heuristics/local_search/*, .../rounding/*, .../constraint_prop.*
Replace timer_t with termination_checker_t across local search and rounding, add deterministic caps/accounting/logging, deterministic candidate ordering/sorting, and strengthen probe/repair synchronization.
Presolve, probing & Papilo
cpp/src/mip_heuristics/presolve/*, .../third_party_presolve.*, .../probing_cache.*
Convert presolve/probing APIs to termination_checker_t, add WORK_LIMIT termination reason, deduplicate probing substitutions, truncate/prune iterations under determinism, and gate presolve methods for deterministic mode.
Problem & solution management
cpp/src/mip_heuristics/problem/*, cpp/src/mip_heuristics/solution/*
Deterministic variable-substitution (sort+reduce), papilo handle override support, remove slack buffers, add solution hashing and handle-aware constructors, and extend B&B callback signature to include origin.
Utilities
cpp/src/utilities/* (seed_generator, determinism_log, copy_helpers, cuda_helpers)
Refactor seed generator to per-thread counter+epoch, add copyable_atomic and make_span helpers, NVTX memory-initialization helpers, and add no-op CUOPT_DETERMINISM_LOG macro/header.
Tests & CI
cpp/tests/mip/*, cpp/tests/CMakeLists.txt, ci/compute-sanitizer-suppressions.xml
Add extensive determinism tests and utilities (callback capture, diversity/local-search/presolve tests, spin-kernel helper), update CMake test registrations, and add ComputeSanitizer suppressions XML.
Misc
cpp/src/mip_heuristics/mip_constants.hpp, cpp/src/math_optimization/solver_settings.cu, skills/cuopt-developer/SKILL.md
Add base work-scale macros, register new solver parameters (work-unit scales, gpu_heur_wait_for_exploration), and document common utilities.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gpudet

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

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (5)
cpp/src/mip_heuristics/local_search/rounding/lb_constraint_prop.cu (1)

753-760: ⚠️ Potential issue | 🔴 Critical

Fix timer type mismatch between apply_round and find_integer.

apply_round (line 704) takes work_limit_timer_t& timer, but at line 729 passes it to find_integer (line 759) which expects timer_t& timer. These are distinct types—work_limit_timer_t is an alias for termination_checker_t, while timer_t is a separate class. Update find_integer to accept work_limit_timer_t& instead of timer_t& to match the caller's signature.

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

In `@cpp/src/mip_heuristics/local_search/rounding/lb_constraint_prop.cu` around
lines 753 - 760, The find_integer method signature currently takes timer_t&
timer but is called from apply_round with a work_limit_timer_t&; update the
signature of lb_constraint_prop_t<i_t, f_t>::find_integer to accept
work_limit_timer_t& timer instead of timer_t& so the types match, and adjust any
declarations/forward-declarations or overloads referencing find_integer to the
same work_limit_timer_t& type (refer to function find_integer and the caller
apply_round for where to align the types).
cpp/src/mip_heuristics/solution/solution.cu (1)

109-125: ⚠️ Potential issue | 🟠 Major

Don't copy scratch buffers that may never have been computed.

mark_span_as_initialized() only suppresses init checking; it does not produce stable bytes. This block still copies lower_excess, upper_excess, constraint_value, obj_val, and n_feasible_constraints even after acknowledging they may be uninitialized, so cloned solutions can inherit arbitrary state and reintroduce nondeterminism before those fields are recomputed.

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

In `@cpp/src/mip_heuristics/solution/solution.cu` around lines 109 - 125, The code
is copying scratch buffers that may be uninitialized (mark_span_as_initialized
only silences checks), so stop blindly copying lower_excess, upper_excess,
constraint_value, obj_val, and n_feasible_constraints from other_sol; instead,
only perform expand_device_copy/raft::copy when the corresponding buffer in
other_sol is actually computed/valid (use whatever validity flags or "computed"
indicators exist on other_sol, e.g., other_sol.lower_excess_computed /
other_sol.constraint_value_valid or similar), and if no validity flags exist
either initialize the destination buffers to deterministic values (e.g., zero)
before returning; update the calls to expand_device_copy and raft::copy to be
guarded by those checks (or replace with deterministic initialization) and keep
mark_span_as_initialized only if you still need to suppress init checks.
cpp/src/mip_heuristics/local_search/local_search.cu (1)

289-317: ⚠️ Potential issue | 🟠 Major

Return the final solution's feasibility, not CPUFJ's.

do_fj_solve may keep the GPU result and then recompute solution's feasibility, but Line 317 returns cpu_feasible. run_fj_on_zero() uses that return value directly, so a GPU-only feasible improvement is reported as failure and can be discarded.

💡 Minimal fix
-  solution.compute_feasibility();
-
-  return cpu_feasible;
+  solution.compute_feasibility();
+  return solution.get_feasible();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/mip_heuristics/local_search/local_search.cu` around lines 289 - 317,
The function do_fj_solve currently returns cpu_feasible which can misreport
success when the final stored solution is the GPU one; after the existing
solution.compute_feasibility() call, change the return to reflect the final
solution's feasibility (e.g., return solution.get_feasible()) instead of
returning cpu_feasible so callers like run_fj_on_zero correctly see the actual
retained solution's feasibility; update any related debug/logs if needed to
reference the final feasibility.
cpp/src/mip_heuristics/diversity/population.cuh (1)

87-95: ⚠️ Potential issue | 🟠 Major

Don't copy best_feasible_objective from the sentinel slot.

indices[0] always exists, even when solutions[0] is still just the placeholder. After Line 94, clear_except_best_feasible() can seed best_feasible_objective from that sentinel and suppress later incumbents/callbacks.

Suggested fix
   void clear_except_best_feasible()
   {
+    const bool had_best_feasible = solutions[indices[0].first].first;
     for (auto& a : solutions) {
       a.first = false;
     }
-    solutions[indices[0].first].first = true;
+    if (had_best_feasible) { solutions[indices[0].first].first = true; }
     indices.erase(indices.begin() + 1, indices.end());
-    best_feasible_objective = solutions[indices[0].first].second.get_objective();
+    best_feasible_objective =
+      had_best_feasible ? solutions[indices[0].first].second.get_objective()
+                        : std::numeric_limits<f_t>::max();
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/mip_heuristics/diversity/population.cuh` around lines 87 - 95, In
clear_except_best_feasible(), do not unconditionally copy
best_feasible_objective from the sentinel slot; after you clear solution flags
and collapse indices you must ensure indices[0].first refers to a real feasible
solution (not the sentinel at slot 0) before assigning best_feasible_objective.
Change the logic in clear_except_best_feasible() (referencing solutions, indices
and best_feasible_objective) so that if indices[0].first == 0 or the referenced
solution is the placeholder, you scan indices for the first non-sentinel
feasible entry and use that solution's .second.get_objective(); if no real
feasible solution exists, leave best_feasible_objective unchanged or set an
appropriate invalid marker instead.
cpp/src/mip_heuristics/diversity/diversity_manager.cu (1)

126-143: ⚠️ Potential issue | 🔴 Critical

Initialize context.gpu_heur_loop.deterministic before the env-gated return.

When CUOPT_CONFIG_ID is unset, Line 127 returns before Lines 142-143 ever run. That leaves context.gpu_heur_loop.deterministic == false, so the new deterministic GPU-heuristics path silently falls back to opportunistic timing/work accounting in the common case.

Suggested fix
+  context.gpu_heur_loop.deterministic =
+    (context.settings.determinism_mode & CUOPT_DETERMINISM_GPU_HEURISTICS);
+
   const char* env_config_id_raw = std::getenv("CUOPT_CONFIG_ID");
   if (env_config_id_raw == nullptr) { return; }
@@
-  context.gpu_heur_loop.deterministic =
-    (context.settings.determinism_mode & CUOPT_DETERMINISM_GPU_HEURISTICS);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/mip_heuristics/diversity/diversity_manager.cu` around lines 126 -
143, The code currently returns early when CUOPT_CONFIG_ID is unset, which skips
initializing context.gpu_heur_loop.deterministic; move or add initialization of
context.gpu_heur_loop.deterministic (using context.settings.determinism_mode &
CUOPT_DETERMINISM_GPU_HEURISTICS) before the getenv() check/early returns so the
deterministic flag is always set regardless of CUOPT_CONFIG_ID parsing or bounds
checks (refer to context.gpu_heur_loop.deterministic, CUOPT_CONFIG_ID, and
CUOPT_DETERMINISM_GPU_HEURISTICS to locate the affected code).
🟡 Minor comments (11)
cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cu-532-534 (1)

532-534: ⚠️ Potential issue | 🟡 Minor

Don’t overwrite the real exit reason on the last allowed iteration.

iter_limit is post-decremented in the while condition, so it is already 0 inside the final permitted iteration. If that iteration breaks for TIME_LIMIT or NO_CANDIDATE, Line 701 rewrites the reason to ITER_LIMIT, which makes the determinism logs misleading.

Also applies to: 701-701

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

In `@cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cu` around lines
532 - 534, The loop uses iter_limit with a post-decrement in the while condition
(symbols: iter_limit, timer.deterministic, the while(h_n_violated_cstr > 0 &&
iter_limit-- > 0) loop) so iter_limit is 0 inside the final allowed iteration;
therefore do not unconditionally overwrite the final exit-reason with ITER_LIMIT
at the end of that iteration. Fix by only setting the exit reason to ITER_LIMIT
when the loop actually terminated due to the iteration limit (e.g., test the
condition after the loop or track whether the last iteration consumed the final
count before assigning ITER_LIMIT), or check the value of iter_limit prior to
decrementing (or use a separate counter/flag) so existing reasons like
TIME_LIMIT or NO_CANDIDATE are preserved.
cpp/src/mip_heuristics/diversity/recombiners/recombiner_stats.hpp-78-84 (1)

78-84: ⚠️ Potential issue | 🟡 Minor

Reset the new last-recombiner fields with the rest of the stats.

all_recombine_stats::reset() still leaves last_recombiner_time and last_recombiner_work untouched, so a fresh run can inherit stale telemetry from the previous one.

Proposed fix
  void reset()
  {
    for (size_t i = 0; i < recombiner_count; ++i) {
      stats[i].reset();
    }
    last_attempt.reset();
+   last_recombiner_time = 0.0;
+   last_recombiner_work = 0.0;
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/mip_heuristics/diversity/recombiners/recombiner_stats.hpp` around
lines 78 - 84, In all_recombine_stats::reset(), clear the newly added telemetry
fields so they don't carry over between runs by explicitly resetting
last_recombiner_time to 0.0, last_recombiner_work to 0.0 and
last_recombiner_start_time to a default-constructed time_point; update the
reset() implementation to assign these default values (alongside the existing
resets) so any new run starts with fresh recombiner stats.
cpp/src/utilities/timer.hpp-37-50 (1)

37-50: ⚠️ Potential issue | 🟡 Minor

Suppress the unused source-location parameters in check_time_limit().

The caller, file, and line parameters are only referenced in commented-out debug code. With -Werror enabled in the build configuration, this will trigger compilation failures on stricter toolchains. The codebase already uses (void) suppression for unused parameters elsewhere (e.g., grpc_service_impl.cpp); apply the same pattern here:

  bool check_time_limit(const char* caller = __builtin_FUNCTION(),
                        const char* file   = __builtin_FILE(),
                        int line           = __builtin_LINE()) const noexcept
  {
+    (void)caller;
+    (void)file;
+    (void)line;
     bool elapsed = elapsed_time() >= time_limit;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/utilities/timer.hpp` around lines 37 - 50, check_time_limit currently
declares parameters caller, file, and line but only uses them in commented debug
code, causing unused-parameter warnings; suppress them by adding explicit
(void)caller; (void)file; (void)line; (or the project's existing
unused-parameter pattern) at the start of check_time_limit's body (the function
named check_time_limit in timer.hpp that calls elapsed_time() and compares to
time_limit) so the compiler no longer complains while leaving the debug prints
intact for future use.
cpp/src/mip_heuristics/presolve/bounds_presolve.cu-175-177 (1)

175-177: ⚠️ Potential issue | 🟡 Minor

In-place modification of settings.iteration_limit may cause unintended side effects.

The determinism check modifies settings.iteration_limit directly. If the same settings object is reused across multiple calls, subsequent calls will see the reduced limit even when determinism mode changes. Consider using a local variable instead.

🔧 Proposed fix
+  i_t effective_iteration_limit = settings.iteration_limit;
   if ((context.settings.determinism_mode & CUOPT_DETERMINISM_GPU_HEURISTICS)) {
-    settings.iteration_limit = std::min(settings.iteration_limit, 50);
+    effective_iteration_limit = std::min(effective_iteration_limit, 50);
   }

   i_t iter;
   upd.init_changed_constraints(pb.handle_ptr);
-  for (iter = 0; iter < settings.iteration_limit; ++iter) {
+  for (iter = 0; iter < effective_iteration_limit; ++iter) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/mip_heuristics/presolve/bounds_presolve.cu` around lines 175 - 177,
The code currently mutates settings.iteration_limit inside the determinism
check, which can leak the reduced value into future calls; instead, introduce a
local variable (e.g., local_iteration_limit) initialized from
settings.iteration_limit, then, if (context.settings.determinism_mode &
CUOPT_DETERMINISM_GPU_HEURISTICS) set local_iteration_limit =
std::min(local_iteration_limit, 50) and use that local variable for subsequent
logic; do not modify settings.iteration_limit so the original settings object
remains unchanged across calls.
cpp/tests/mip/feasibility_jump_tests.cu-197-198 (1)

197-198: ⚠️ Potential issue | 🟡 Minor

Tolerance of 1.0 for determinism check is inconsistent with determinism testing standards.

The dedicated determinism tests in determinism_test.cu use EXPECT_DOUBLE_EQ (exact equality) for objective comparisons across multiple runs with the same seed. Using EXPECT_NEAR(..., 1.0) here allows a 1.0 absolute difference, which is contradictory to true determinism. If the seed ensures reproducibility, the tolerance should be much tighter (e.g., 1e-9) or use exact equality like the standard determinism tests. Other FJ test tolerances use 1e-4, making 1.0 appear to be an oversight.

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

In `@cpp/tests/mip/feasibility_jump_tests.cu` around lines 197 - 198, The test
uses EXPECT_NEAR(solution.get_user_objective(), first_val_map[test_instance],
1.0) which permits a large 1.0 difference and contradicts determinism
expectations; change this to strict equality like
EXPECT_DOUBLE_EQ(solution.get_user_objective(), first_val_map[test_instance])
(or, if you prefer a tiny tolerance consistent with other FJ tests, use
EXPECT_NEAR(..., 1e-9)) so the determinism check in this test matches the
exactness used in determinism_test.cu; update the assertion referencing
solution.get_user_objective(), first_val_map[test_instance], and test_instance
accordingly.
cpp/tests/mip/determinism_test.cu-454-454 (1)

454-454: ⚠️ Potential issue | 🟡 Minor

Fix typo in comment.

Minor typo: "work limiy" should be "work limit".

📝 Suggested fix
-    // Instance, threads, time_limit, work limiy
+    // Instance, threads, time_limit, work_limit
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/tests/mip/determinism_test.cu` at line 454, Fix the typo in the comment
that currently reads "work limiy" by changing it to "work limit" so the comment
reads "Instance, threads, time_limit, work limit"; locate the comment near the
test in determinism_test.cu (around the instance/threads/time_limit mention) and
update the wording accordingly.
cpp/tests/mip/determinism_utils.cuh-64-69 (1)

64-69: ⚠️ Potential issue | 🟡 Minor

Potential race between async flag write and thread termination check.

The destructor sets the flag asynchronously (set_value_async) but then immediately joins the thread. If the spin kernel times out and the loop iterates before the async write becomes visible, there could be an extra iteration. Consider adding a stream sync before join for deterministic teardown.

🛡️ Suggested fix
   ~spin_stream_raii_t()
   {
     int one = 1;
     flag.set_value_async(one, stream);
+    cudaStreamSynchronize(stream.view());
     spin_thread.join();
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/tests/mip/determinism_utils.cuh` around lines 64 - 69, Destructor
spin_stream_raii_t currently calls flag.set_value_async(one, stream) then
immediately spin_thread.join(), risking the spin kernel seeing stale flag due to
asynchronous write; after calling flag.set_value_async(...) synchronize the CUDA
stream (e.g., cudaStreamSynchronize(stream) or stream.synchronize()) before
calling spin_thread.join() so the flag write is visible to the device before the
thread termination check (alternatively use a blocking set_value if available).
cpp/tests/mip/diversity_test.cu-293-300 (1)

293-300: ⚠️ Potential issue | 🟡 Minor

Apply CUOPT_INSTANCE before computing path.

path is derived before the environment override is read, so these two active tests can print one instance name while still opening another file. That makes local repro/debugging misleading.

💡 Proposed fix
-  auto path     = make_path_absolute(test_instance);
-  test_instance = std::getenv("CUOPT_INSTANCE") ? std::getenv("CUOPT_INSTANCE") : test_instance;
+  if (const char* instance = std::getenv("CUOPT_INSTANCE")) {
+    test_instance = instance;
+  }
+  auto path = make_path_absolute(test_instance);

Also applies to: 322-329

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

In `@cpp/tests/mip/diversity_test.cu` around lines 293 - 300, The code computes
path from test_instance before honoring the CUOPT_INSTANCE override, causing a
mismatch between printed instance and file opened; move the CUOPT_INSTANCE
handling (the assignment to test_instance from getenv("CUOPT_INSTANCE")) to
occur before calling make_path_absolute so that path =
make_path_absolute(test_instance) uses the possibly overridden value, and apply
the same reorder for the similar block around the code using
make_path_absolute/test_instance at lines 322-329.
cpp/include/cuopt/linear_programming/utilities/internals.hpp-26-70 (1)

26-70: ⚠️ Potential issue | 🟡 Minor

Document the new public callback metadata and extended callback API.

mip_solution_origin_t, mip_solution_callback_info_t, and get_solution_callback_ext_t add new public C++ API surface, but there are no Doxygen comments explaining origin, work_timestamp, callback lifetime/ownership, or threading expectations. Please document that contract in the header and the corresponding user-facing docs/migration notes.

As per coding guidelines, cpp/include/cuopt/**/*: "For public header files (C++ API): Check if new public functions/classes have documentation comments (Doxygen format)", "Flag API changes that may need corresponding docs/ updates", and "Suggest documenting thread-safety, GPU requirements, and numerical behavior."

Also applies to: 104-115

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

In `@cpp/include/cuopt/linear_programming/utilities/internals.hpp` around lines 26
- 70, The new public API types (mip_solution_origin_t,
mip_solution_callback_info_t, and get_solution_callback_ext_t) and callback enum
(base_solution_callback_type) lack required Doxygen/consumer-facing
documentation; please add Doxygen comments in this header describing the
semantics of origin and work_timestamp fields, callback lifetime/ownership
expectations, thread-safety and GPU/host execution constraints, numerical
behavior/units, and migration notes for the extended get_solution callback
(GET_SOLUTION_EXT), and also update user-facing docs to mention these changes
and any required client-side adaptations.
cpp/tests/mip/presolve_test.cu-138-144 (1)

138-144: ⚠️ Potential issue | 🟡 Minor

Hash both probing directions.

The composite hash only includes a.second[0].var_to_cached_bound_map. A nondeterministic change in the second cached probe entry will still pass this test.

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

In `@cpp/tests/mip/presolve_test.cu` around lines 138 - 144, The test currently
only iterates and hashes entries from a.second[0].var_to_cached_bound_map
(creating sorted_map and appending to var_to_cached_bound_keys,
var_to_cached_bound_lb, var_to_cached_bound_ub), which misses nondeterministic
changes in the second probe; change the logic to combine and iterate
deterministic entries from both probe directions (both
a.second[0].var_to_cached_bound_map and a.second[1].var_to_cached_bound_map) —
e.g., merge or process both maps into a single sorted container before pushing
into var_to_cached_bound_keys, var_to_cached_bound_lb, and
var_to_cached_bound_ub so the composite hash includes both probe directions.
cpp/src/mip_heuristics/local_search/local_search.cuh-80-83 (1)

80-83: ⚠️ Potential issue | 🟡 Minor

The default population_ptr is misleading.

The implementation asserts population_ptr != nullptr, so this new = nullptr default turns a valid-looking call site into a runtime abort.

💡 Tighten the API contract
-  bool run_fp(solution_t<i_t, f_t>& solution,
-              work_limit_timer_t& timer,
-              population_t<i_t, f_t>* population_ptr = nullptr,
-              i_t n_fp_iterations                    = std::numeric_limits<i_t>::max());
+  bool run_fp(solution_t<i_t, f_t>& solution,
+              work_limit_timer_t& timer,
+              population_t<i_t, f_t>* population_ptr,
+              i_t n_fp_iterations = std::numeric_limits<i_t>::max());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/mip_heuristics/local_search/local_search.cuh` around lines 80 - 83,
The declaration of run_fp currently gives population_ptr a default of nullptr
but the implementation asserts population_ptr != nullptr, which makes the
default misleading; fix by tightening the API contract — remove the "= nullptr"
default so the signature is bool run_fp(solution_t<i_t, f_t>& solution,
work_limit_timer_t& timer, population_t<i_t, f_t>* population_ptr, i_t
n_fp_iterations = std::numeric_limits<i_t>::max()); or alternatively change the
parameter to a non-null reference (population_t<i_t, f_t>& population) if
callers must always supply a population, and update all call sites accordingly
to ensure no null pointer is passed to run_fp.
🧹 Nitpick comments (23)
cpp/CMakeLists.txt (1)

546-556: Remove commented PAPI code or make it optional.

The commented-out PAPI integration block doesn't affect the current build. Consider either:

  1. Preferred: Remove it entirely and rely on version control history.
  2. Alternative: If PAPI support is planned, wrap it in a CMake option and use find_package() for better robustness:
♻️ Proposed optional PAPI integration
-# find_path(PAPI_INCLUDE_DIR papi.h)
-# find_library(PAPI_LIBRARY papi)
-
-# if (PAPI_INCLUDE_DIR AND PAPI_LIBRARY)
-#     message(STATUS "Found PAPI in ${PAPI_INCLUDE_DIR}")
-#     target_include_directories(cuopt PRIVATE ${PAPI_INCLUDE_DIR})
-#     target_link_libraries(cuopt PRIVATE ${PAPI_LIBRARY})
-# else()
-#     message(FATAL_ERROR "Could not find PAPI")
-# endif()
+option(USE_PAPI "Enable PAPI performance monitoring support" OFF)
+
+if(USE_PAPI)
+  find_package(PAPI)
+  if(PAPI_FOUND)
+    message(STATUS "Found PAPI: ${PAPI_INCLUDE_DIRS}")
+    target_include_directories(cuopt PRIVATE ${PAPI_INCLUDE_DIRS})
+    target_link_libraries(cuopt PRIVATE ${PAPI_LIBRARIES})
+    target_compile_definitions(cuopt PRIVATE CUOPT_ENABLE_PAPI)
+  else()
+    message(WARNING "PAPI requested but not found. Continuing without PAPI support.")
+  endif()
+endif()

Note: This approach requires a FindPAPI.cmake module or PAPI providing a CMake config file.

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

In `@cpp/CMakeLists.txt` around lines 546 - 556, The commented PAPI block should
be removed or made optional: either delete the entire commented block
referencing PAPI (the find_path/find_library checks and the conditional linking
to target 'cuopt') to rely on VCS history, or convert it to a proper optional
integration by adding a CMake option (e.g., USE_PAPI) and guarded logic that
uses find_package()/FindPAPI to set PAPI_INCLUDE_DIR and PAPI_LIBRARY and
conditionally calls target_include_directories(cuopt PRIVATE ...) and
target_link_libraries(cuopt PRIVATE ...); update the CMake logic around the
'cuopt' target accordingly and ensure a clear message when PAPI is enabled but
not found.
ci/compute-sanitizer-suppressions.xml (1)

70-79: Consider adding host stack constraints to kernel-only suppression.

This record currently keys only on DeviceSegmentedReduceKernel and read size. Adding a minimal hostStack signature would reduce accidental matches across unrelated kernels with similar symbol names.

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

In `@ci/compute-sanitizer-suppressions.xml` around lines 70 - 79, The suppression
currently matches only on DeviceSegmentedReduceKernel and size; tighten it by
adding a minimal hostStack signature to the same <record> so the suppress
targets the specific launcher/caller frame on the host (e.g., the host-side
kernel-launch wrapper) rather than any kernel with that symbol; locate the
<record> for DeviceSegmentedReduceKernel and add a <hostStack> (with one <frame>
containing a short, unique host function name or signature present in the stack
when this kernel is launched) to reduce accidental matches while keeping the
kernel-only suppression semantics.
cpp/src/mip_heuristics/diversity/weights.cuh (1)

30-33: Hashing method for determinism tracing looks correct.

The XOR combination of component hashes provides a reasonable fingerprint for debugging determinism.

One consideration: objective_weight.value(stream) performs a synchronous device-to-host transfer. If get_hash is called frequently in hot paths, this could impact performance. Since this appears to be for determinism debugging (likely behind CUOPT_DETERMINISM_LOG), this is acceptable, but ensure it's not called in production code paths.

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

In `@cpp/src/mip_heuristics/diversity/weights.cuh` around lines 30 - 33, get_hash
currently calls objective_weight.value(stream) which forces a synchronous
device-to-host transfer; to avoid hot-path stalls, ensure this is only used for
determinism debugging by guarding the synchronous call (or the whole get_hash)
behind the determinism logging/config flag (e.g., CUOPT_DETERMINISM_LOG) or
replace the sync transfer with a device-side/streamed hash using compute_hash on
the device view. Locate get_hash, compute_hash and objective_weight.value and
either wrap the call in the determinism-only preprocessor/runtime guard or
change the implementation to compute the hash without calling
objective_weight.value(stream).
cpp/src/utilities/seed_generator.cuh (1)

38-48: Potential data race on base_seed_ if set_seed is called concurrently.

base_seed_ is a non-atomic int64_t that is written in set_seed() and read in local_state(). While epoch_ correctly uses atomic operations with appropriate memory ordering, concurrent calls to set_seed() from different threads would create a data race on base_seed_.

If set_seed is guaranteed to be called only from a single thread (e.g., at initialization before solver starts), this is fine. Otherwise, consider making base_seed_ atomic:

♻️ Suggested fix if concurrent set_seed is possible
 class seed_generator {
-  static int64_t base_seed_;
+  static std::atomic<int64_t> base_seed_;
   static std::atomic<int64_t> epoch_;

And update the accesses accordingly.

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

In `@cpp/src/utilities/seed_generator.cuh` around lines 38 - 48, The code has a
data race on base_seed_ because set_seed() writes a non-atomic int64_t while
local_state() reads it; change base_seed_ to an atomic<int64_t> (or protect it
with a mutex) and update all accesses: in set_seed() use base_seed_.store(value,
std::memory_order_release) (or equivalent) and in local_state() use
base_seed_.load(std::memory_order_acquire); keep epoch_ semantics as-is so
ordering between epoch_ and base_seed_ is preserved.
cpp/src/mip_heuristics/feasibility_jump/utils.cuh (1)

137-152: Minor: Redundant stream synchronizations from repeated set_size.value(stream) calls.

set_size.value(stream) synchronizes with the device to retrieve the scalar value. This method calls it three times (lines 140, 144, and potentially 150), causing multiple unnecessary synchronizations.

Consider caching the value once:

♻️ Suggested optimization
 void sort(const rmm::cuda_stream_view& stream)
 {
+  i_t current_size = set_size.value(stream);
   thrust::sort(
-    rmm::exec_policy(stream), contents.begin(), contents.begin() + set_size.value(stream));
+    rmm::exec_policy(stream), contents.begin(), contents.begin() + current_size);
   thrust::fill(rmm::exec_policy(stream), index_map.begin(), index_map.end(), -1);
   thrust::for_each(rmm::exec_policy(stream),
                    thrust::make_counting_iterator<i_t>(0),
-                   thrust::make_counting_iterator<i_t>(set_size.value(stream)),
+                   thrust::make_counting_iterator<i_t>(current_size),
                    [v = view()] __device__(i_t idx) { v.index_map[v.contents[idx]] = idx; });

   // only useful for debugging and ensuring the same hashes are printed
 `#if` FJ_SINGLE_STEP
   thrust::fill(
-    rmm::exec_policy(stream), contents.begin() + set_size.value(stream), contents.end(), 0);
+    rmm::exec_policy(stream), contents.begin() + current_size, contents.end(), 0);
 `#endif`
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/mip_heuristics/feasibility_jump/utils.cuh` around lines 137 - 152,
The sort(...) method calls set_size.value(stream) multiple times causing
redundant device synchronizations; cache the result once (e.g., auto cached_size
= set_size.value(stream)) at the start of sort and use cached_size for the
thrust::sort end iterator, thrust::for_each counting_iterator upper bound, and
the conditional thrust::fill start offset, ensuring the lambda and iterators use
the cached_size value instead of calling set_size.value(stream) repeatedly;
update any captures/iterator bounds accordingly (symbols to change: sort,
set_size.value(stream), contents, index_map, view()).
cpp/src/utilities/cuda_helpers.cuh (1)

24-34: Note: TODO comment about NVTX dependency conflict.

The TODO on line 25 mentions a potential dependency conflict that needs investigation. The current workaround (defining NVTX_NULLPTR and NVTX_REINTERPRET_CAST if missing) appears to work, but tracking this for a proper fix would be beneficial.

Would you like me to open an issue to track the NVTX dependency conflict investigation?

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

In `@cpp/src/utilities/cuda_helpers.cuh` around lines 24 - 34, Replace the vague
TODO with an actionable FIXME that references a new issue tracking the NVTX
dependency conflict: create an issue in your tracker describing why
<nvtx3/nvtx3.hpp> and <nvtx3/nvToolsExtMemCudaRt.h> require the fallback defines
(NVTX_NULLPTR and NVTX_REINTERPRET_CAST), include the issue ID in the source
comment, and keep the current fallback defines (NVTX_NULLPTR and
NVTX_REINTERPRET_CAST) as a documented workaround until the issue is resolved;
ensure the comment names the headers and symbols (nvtx3/nvtx3.hpp,
nvtx3/nvToolsExtMemCudaRt.h, NVTX_NULLPTR, NVTX_REINTERPRET_CAST) and starts
with FIXME so it’s easy to find later.
cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cuh (1)

14-15: Add a direct include for work_limit_timer_t in this header.

Since this header now declares work_limit_timer_t in public API/state (Line 126, Line 150), prefer including utilities/work_limit_timer.hpp directly instead of relying on transitive includes.

♻️ Proposed include cleanup
-#include <utilities/timer.hpp>
+#include <utilities/work_limit_timer.hpp>

Also applies to: 126-127, 150-150

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

In `@cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cuh` around lines
14 - 15, The header currently relies on a transitive include for
work_limit_timer_t (used in the public API/state around the declarations
referencing work_limit_timer_t); update the includes in
rounding/bounds_repair.cuh to directly include utilities/work_limit_timer.hpp
(in addition to or replacing utilities/timer.hpp as appropriate) so the type is
explicitly available for the public declarations that reference
work_limit_timer_t.
cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu (1)

47-59: Consider graceful handling of invalid environment variable values.

The read_positive_work_unit_scale function uses cuopt_assert (line 57) when the environment variable value is invalid. This will abort the program on malformed input, which may be surprising to users who misconfigure the environment.

Consider either:

  1. Logging a warning and returning the default 1.0 for invalid values, or
  2. Throwing an exception that can be caught at a higher level

This provides a better user experience while still surfacing configuration errors.

♻️ Suggested approach for graceful error handling
   const bool valid_value    = errno == 0 && end_ptr != env_value && *end_ptr == '\0' &&
                            std::isfinite(parsed_value) && parsed_value > 0.0;
-  cuopt_assert(valid_value, "Invalid CPUFJ work-unit scale env var");
+  if (!valid_value) {
+    CUOPT_LOG_WARN("Invalid CUOPT_CPUFJ_WORK_UNIT_SCALE value '%s', using default 1.0", env_value);
+    return 1.0;
+  }
   return parsed_value;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu` around lines 47 - 59, The
function read_positive_work_unit_scale currently calls cuopt_assert on invalid
or non-positive env var values which aborts the program; change it to handle
malformed input gracefully by validating parsed_value and, on failure, either
(a) log a warning (using the project's logging facility) and return the default
1.0, or (b) throw a std::runtime_error with a clear message so callers can catch
it; replace the cuopt_assert(valid_value, ...) call with that chosen behavior,
keeping the rest of parsing code (errno/reset, std::strtod, isfinite, >0.0)
intact and referencing read_positive_work_unit_scale and cuopt_assert in your
edit.
cpp/src/mip_heuristics/local_search/rounding/lb_constraint_prop.cu (1)

36-37: Constructor initializes max_timer with placeholder values.

The max_timer member is initialized with (0.0, root_tag_t{}) in the constructor and later reconfigured in apply_round with actual context values. This two-phase initialization is acceptable given the context isn't available at construction time, but consider adding a brief comment explaining this pattern for future maintainers.

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

In `@cpp/src/mip_heuristics/local_search/rounding/lb_constraint_prop.cu` around
lines 36 - 37, The constructor currently initializes max_timer with placeholder
values (max_timer(0.0, cuopt::termination_checker_t::root_tag_t{})) because the
real context is only available later in apply_round; add a brief comment in the
constructor next to the max_timer initialization explaining this two-phase
initialization pattern and that apply_round reconfigures max_timer with the real
context so future maintainers won't remove or misunderstand the placeholder init
(refer to max_timer, the constructor, and apply_round).
cpp/tests/mip/CMakeLists.txt (1)

43-57: Update the stale enablement comment.

With FEASIBILITY_JUMP_TEST, LOCAL_SEARCH_TEST, and DIVERSITY_TEST registered here, the nearby # Disable for now comment is now misleading.

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

In `@cpp/tests/mip/CMakeLists.txt` around lines 43 - 57, The nearby "Disable for
now" comment is now incorrect because tests FEASIBILITY_JUMP_TEST,
LOCAL_SEARCH_TEST, and DIVERSITY_TEST are being registered; update or remove
that stale comment near the ConfigureTest entries (specifically referencing
FEASIBILITY_JUMP_TEST, LOCAL_SEARCH_TEST, and DIVERSITY_TEST) so the
CMakeLists.txt accurately reflects that these tests are enabled rather than
disabled.
cpp/src/mip_heuristics/diversity/multi_armed_bandit.cuh (1)

55-64: Consider extracting magic numbers as named constants.

The divisors 2000 (for time-based) and 200 (for work-based) represent expected recombiner cost. Extracting these as named constants would improve maintainability and clarify the 10:1 ratio between time and work units.

♻️ Suggested refactor
+constexpr double recombiner_expected_time_ms = 2000.0;
+constexpr double recombiner_expected_work_units = 200.0;
+
 struct recombiner_work_normalized_reward_t {
   bool deterministic;
   double work;
   // ...
   double operator()(double factor) const
   {
-    // normal recombiners take 2000 ms
     if (!deterministic) {
       double time_in_miliseconds = work;
-      return factor * (std::max(0.1, 4.0 - (time_in_miliseconds / 2000)));
+      return factor * (std::max(0.1, 4.0 - (time_in_miliseconds / recombiner_expected_time_ms)));
     } else {
-      return factor * (std::max(0.1, 4.0 - (work / 200)));
+      return factor * (std::max(0.1, 4.0 - (work / recombiner_expected_work_units)));
     }
   }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/mip_heuristics/diversity/multi_armed_bandit.cuh` around lines 55 -
64, The operator() in multi_armed_bandit.cuh uses magic numbers 2000 and 200 to
scale factor based on work/time; extract these into clearly named constants
(e.g., kRecombinerTimeMs = 2000 and kRecombinerWorkUnits = 200) and replace the
literals in the deterministic and non-deterministic branches (operator(),
deterministic, work, time_in_miliseconds) so the 10:1 ratio is explicit and
maintainable; keep the existing max(0.1, ...) logic and ensure the new constants
are declared near the class or in an anonymous namespace for clarity.
cpp/src/mip_heuristics/diversity/recombiners/line_segment_recombiner.cuh (1)

87-88: Address the TODO: work calculation appears provisional.

The work formula n_different_vars / 1e8 is marked with a TODO comment. This heuristic maps 100 million different variables to 1 work unit, which may need calibration based on actual runtime characteristics.

Would you like me to open an issue to track refining this work calculation formula?

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

In `@cpp/src/mip_heuristics/diversity/recombiners/line_segment_recombiner.cuh`
around lines 87 - 88, The temporary work calculation uses a magic constant
(double work = static_cast<double>(n_different_vars) / 1e8) in
line_segment_recombiner.cuh; replace this TODO by computing work from a named,
configurable scaling factor (e.g., WORK_PER_UNIT or work_scale) or a function
that derives scale from problem characteristics (n_different_vars, thread/CPU
count, or calibrated benchmark constant) and use that instead of 1e8 so the
value can be tuned at runtime or via build-time constexpr; update references to
the local variable work and ensure the scale is documented and exposed where
recombine workload budgeting (in the LineSegmentRecombiner or equivalent
function) is performed.
cpp/include/cuopt/linear_programming/mip/solver_settings.hpp (1)

110-116: Document these new public knobs in Doxygen and release docs.

These fields and the new bitset semantics on determinism_mode are part of the public C++ API now, but callers only get inline comments. Please document valid ranges/defaults, whether the work-unit scales must stay positive, and how gpu_heur_wait_for_exploration behaves with heuristics_only and deterministic runs.

As per coding guidelines, cpp/include/cuopt/**/*: "For public header files (C++ API): Check if new public functions/classes have documentation comments (Doxygen format) ... Suggest documenting thread-safety, GPU requirements, and numerical behavior."

Also applies to: 127-136

🤖 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
110 - 116, Add Doxygen comments for the new public members
cpufj_work_unit_scale, gpu_heur_work_unit_scale, bb_work_unit_scale, and
gpu_heur_wait_for_exploration in the header (and mirror them in the release
docs): state default values (1.0), valid ranges (positive; e.g. >0.0), and that
they are multiplicative modifiers on internal work-unit scaling; document that
determinism_mode uses bitset semantics (describe bits) and how
gpu_heur_wait_for_exploration interacts with heuristics_only and deterministic
runs (GPU heuristics will either wait for root B&B finish or start immediately
depending on this flag and determinism settings); also note thread-safety
expectations, GPU requirements for gpu_heur_* options, and any
numerical/behavioral effects of non-default scales for reproducibility and
performance tuning.
benchmarks/linear_programming/cuopt/run_mip.cpp (1)

264-287: Use canonical determinism and scaling constants for better API clarity.

The legacy CUOPT_MODE_* aliases and boolean assignment for mip_scaling work because they're defined in constants.h, but the API headers define these fields with the canonical CUOPT_DETERMINISM_* and CUOPT_MIP_SCALING_* names. Update this benchmark to use:

  • CUOPT_DETERMINISM_GPU_HEURISTICS and CUOPT_DETERMINISM_FULL instead of CUOPT_MODE_DETERMINISTIC_GPU_HEURISTICS and CUOPT_MODE_DETERMINISTIC
  • CUOPT_DETERMINISM_NONE instead of CUOPT_MODE_OPPORTUNISTIC
  • CUOPT_MIP_SCALING_OFF instead of false for consistency with the int field type
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmarks/linear_programming/cuopt/run_mip.cpp` around lines 264 - 287,
Replace legacy CUOPT_MODE_* and boolean assignment with the canonical
determinism and scaling constants: set settings.determinism_mode to
CUOPT_DETERMINISM_GPU_HEURISTICS when heuristics_only is true and
CUOPT_DETERMINISM_FULL when deterministic is true (instead of
CUOPT_MODE_DETERMINISTIC_GPU_HEURISTICS / CUOPT_MODE_DETERMINISTIC), set the
else branch to CUOPT_DETERMINISM_NONE (instead of CUOPT_MODE_OPPORTUNISTIC), and
change settings.mip_scaling from false to CUOPT_MIP_SCALING_OFF so the int-typed
field uses the canonical CUOPT_MIP_SCALING_* constant.
cpp/tests/mip/feasibility_jump_tests.cu (1)

193-198: Static map may cause issues with parallel test execution.

The static std::unordered_map<std::string, double> first_val_map persists across test invocations. If tests run in parallel or the test binary is reused, this could lead to unexpected behavior. Consider using a test fixture with member state instead.

♻️ Consider using test fixture for state
-static bool run_fj_check_determinism(std::string test_instance, int iter_limit)
+// In a test fixture class:
+class FeasibilityJumpDeterminismTest : public ::testing::Test {
+ protected:
+  std::unordered_map<std::string, double> first_val_map;
+  
+  bool run_fj_check_determinism(std::string test_instance, int iter_limit) {
+    // ... implementation using this->first_val_map ...
+  }
+};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/tests/mip/feasibility_jump_tests.cu` around lines 193 - 198, The static
map first_val_map persists across runs and can cause race/contamination when
tests execute in parallel; replace the static with per-test fixture state by
moving first_val_map into a test fixture class (e.g., a new FeasibilityJumpTest
fixture) as a member variable and use that fixture in the test so you
access/modify first_val_map via the fixture instead of a static, keeping the
same logic around test_instance and solution.get_user_objective() to set and
compare the initial value.
cpp/include/cuopt/linear_programming/cuopt_c.h (1)

786-797: Consider adding thread-safety documentation for the extended callback API.

The new cuOptSetMIPGetSolutionCallbackExt function is documented, but per coding guidelines for public headers, consider documenting thread-safety guarantees—specifically whether the callback may be invoked concurrently from multiple threads or is always serialized.

As per coding guidelines: "For public header files (C++ API): Suggest documenting thread-safety, GPU requirements, and numerical behavior."

📝 Suggested documentation addition
 /**
  * `@brief` Register an extended callback to receive incumbent MIP solutions with extended metadata.
  *
  * `@param`[in] settings - The solver settings object.
  * `@param`[in] callback - Callback function to receive incumbent solutions and callback metadata.
  * `@param`[in] user_data - User-defined pointer passed through to the callback.
  *
+ * `@note` The callback is invoked from the solver thread. In deterministic mode, callbacks are
+ *       serialized in work-timestamp order. In non-deterministic mode, callback ordering reflects
+ *       solution discovery order but is not guaranteed to be reproducible across runs.
+ *
  * `@return` A status code indicating success or failure.
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/include/cuopt/linear_programming/cuopt_c.h` around lines 786 - 797,
Update the docblock for cuOptSetMIPGetSolutionCallbackExt to state the
thread-safety and concurrency guarantees: explicitly say whether
cuOptMIPGetSolutionCallbackExt callbacks may be invoked concurrently from
multiple threads or are serialized, note any GPU/host thread context
requirements, and instruct that user_data and callback implementations must be
thread-safe if concurrent invocation is possible (or that synchronization is not
required if serialized). Add a short sentence about numerical/precision
expectations if relevant and where to perform synchronization (inside the
callback) to help callers implement safe handlers; place this updated text
directly in the function comment for cuOptSetMIPGetSolutionCallbackExt.
cpp/src/mip_heuristics/relaxed_lp/relaxed_lp.cu (1)

74-97: Iteration estimation model is acknowledged as temporary.

The hardcoded linear model coefficients (lines 82-84) for estimating PDLP iterations are fragile and problem-dependent. The multiple TODO comments acknowledging this as "a rather ugly hack" and "use an actual predictor model here" indicate awareness that this needs improvement.

Consider tracking this as technical debt to ensure it's addressed before production use of deterministic mode at scale.

Would you like me to open an issue to track replacing this hardcoded estimation model with a proper predictor?

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

In `@cpp/src/mip_heuristics/relaxed_lp/relaxed_lp.cu` around lines 74 - 97, The
estimation for PDLP iterations inside the determinism_mode block (variables
estim_iters, op_problem, settings.work_limit and pdlp_settings) uses a fragile
hardcoded linear model; replace this by extracting the coefficients/model into a
configurable/persisted predictor (e.g., a Predictor class or pluggable callback)
and load a default model from config or telemetry so production deterministic
mode can be tuned without code changes, add a clear TODO comment referencing a
new tracking issue ID and unit tests for the predictor path, and ensure the code
falls back to a sensible safe default (e.g., numeric_limits<int>::max() or a
conservative cap) when no predictor is available.
cpp/src/mip_heuristics/presolve/probing_cache.cu (1)

866-870: Consider documenting the magic numbers for deterministic mode limits.

The hardcoded values (iteration_limit = 1 and 2048 max priority indices) constrain behavior significantly in deterministic mode. Consider adding a brief comment explaining the rationale or making these configurable.

💡 Suggested documentation
   // TODO: proper work unit accounting in deterministic mode for the probing cache
   if ((bound_presolve.context.settings.determinism_mode & CUOPT_DETERMINISM_GPU_HEURISTICS)) {
+    // In deterministic mode, limit iterations and variable count to ensure reproducible work
+    // These limits balance determinism guarantees with reasonable presolve coverage
     bound_presolve.settings.iteration_limit = 1;
     priority_indices.resize(std::min<size_t>(priority_indices.size(), 2048));
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/mip_heuristics/presolve/probing_cache.cu` around lines 866 - 870, The
deterministic-mode branch in bound_presolve (checking
bound_presolve.context.settings.determinism_mode &
CUOPT_DETERMINISM_GPU_HEURISTICS) uses magic values—setting
bound_presolve.settings.iteration_limit = 1 and capping priority_indices via
priority_indices.resize(..., 2048)—without explanation; update the code by
either making these values configurable (expose them as settings or constants)
or add a concise comment justifying the chosen limits (why iteration_limit must
be 1 and why 2048 was chosen) and reference the symbols
bound_presolve.context.settings.determinism_mode,
CUOPT_DETERMINISM_GPU_HEURISTICS, bound_presolve.settings.iteration_limit, and
priority_indices.resize to locate the change.
cpp/src/mip_heuristics/diversity/recombiners/fp_recombiner.cuh (1)

68-69: Address the TODO for work unit calculation.

The work value calculation n_vars_from_other / 1e8 has a TODO marker. The magic constant and scaling should be documented or made configurable for proper work-unit accounting.

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

In `@cpp/src/mip_heuristics/diversity/recombiners/fp_recombiner.cuh` around lines
68 - 69, The current TODO indicates the magic scaling in computing work (double
work = static_cast<double>(n_vars_from_other) / 1e8) is placeholder; replace
this with a named, configurable scaling factor (e.g., WORK_UNIT_PER_VAR or
workUnitPerVar) and compute work as n_vars_from_other * workUnitPerVar, or
accept the scaling as a parameter (constructor/func arg) to the FP recombiner
logic in fp_recombiner.cuh; also add a one-line comment documenting the
units/meaning of the factor so the work-unit accounting is explicit and
adjustable.
cpp/tests/mip/determinism_utils.cuh (1)

45-55: Note: Non-seeded rand() may cause test flakiness.

The launch_spin_kernel_stream_thread uses rand() without explicit seeding, which means thread-local default seeds apply. This is likely intentional for stress testing varied launch configs, but worth noting if reproducibility issues arise.

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

In `@cpp/tests/mip/determinism_utils.cuh` around lines 45 - 55, The use of
non-seeded rand() in launch_spin_kernel_stream_thread makes test runs
non-deterministic; replace rand() with a deterministic, thread-local RNG (e.g.,
create a std::mt19937 rng seeded at thread start or accept a seed parameter) and
use std::uniform_int_distribution for blocks/threads and sleep duration so the
launch_spin_kernel_stream_thread behavior is reproducible; ensure the thread
initializes its RNG before calling spin_kernel and checking host_copy so each
thread's sequence is deterministic.
cpp/tests/mip/mip_utils.cuh (1)

231-236: Consider adding a safety check for climber access.

The code accesses fj.climbers[0] without verifying the vector is non-empty. While this may be guaranteed by the FJ initialization, an explicit check would prevent subtle test failures.

💡 Suggested defensive check
   fj.solve(solution);
   auto solution_vector = host_copy(solution.assignment, solution.handle_ptr->get_stream());

+  cuopt_assert(!fj.climbers.empty(), "FJ must have at least one climber");
   return {solution,
           solution_vector,
           fj.climbers[0]->local_minimums_reached.value(solution.handle_ptr->get_stream()),
           fj.climbers[0]->incumbent_objective.value(solution.handle_ptr->get_stream()),
           fj.climbers[0]->violation_score.value(solution.handle_ptr->get_stream())};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/tests/mip/mip_utils.cuh` around lines 231 - 236, The return block reads
fj.climbers[0] without verifying the climbers vector is non-empty; add a
defensive check before that access in the function that builds the tuple (the
block returning {solution, solution_vector, ...}) to ensure fj.climbers.size() >
0 (or !fj.climbers.empty()), and if empty either assert/throw a clear
runtime_error or return a safe default tuple/report error via the test harness;
update the code paths that call fj.climbers[0]->local_minimums_reached,
->incumbent_objective, and ->violation_score to only run after this check.
cpp/tests/mip/diversity_test.cu (1)

266-278: Remove the unreachable tail after the first return.

Everything below return detail::compute_hash(hashes); is dead, so it never contributes to the observable result and makes the helper harder to reason about.

💡 Proposed cleanup
   }
-  return detail::compute_hash(hashes);
-
-  auto pop = diversity_manager.get_population_pointer();
-  for (const auto& sol : pop->population_to_vector()) {
-    hashes.push_back(sol.get_hash());
-  }
-
-  uint32_t final_hash = detail::compute_hash(hashes);
-  printf("%s: final hash: 0x%x, pop size %d\n",
-         path.c_str(),
-         final_hash,
-         (int)pop->population_to_vector().size());
-  return final_hash;
+  return detail::compute_hash(hashes);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/tests/mip/diversity_test.cu` around lines 266 - 278, The function returns
immediately with detail::compute_hash(hashes), making the subsequent block that
accesses diversity_manager.get_population_pointer(), iterates
pop->population_to_vector(), computes final_hash and prints using path
unreachable; remove that dead tail (the loop, final_hash, printf and the second
return) or alternatively move the return to the end so the population-based hash
and printf are executed — update code around detail::compute_hash(hashes),
diversity_manager.get_population_pointer(), pop->population_to_vector(),
final_hash and printf to ensure only the intended return remains.
cpp/src/branch_and_bound/branch_and_bound.cpp (1)

3670-3670: Consider optimizing edge norms copy.

The full vector copy of edge_norms_ at line 3670 occurs for every node solve. If edge norms are frequently large, this could be optimized by tracking whether the worker's copy was modified and only copying when necessary. However, this matches the existing pattern in the non-deterministic path (line 1704), so it's acceptable for now.

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

In `@cpp/src/branch_and_bound/branch_and_bound.cpp` at line 3670, The current code
always copies the full edge_norms_ vector for every node solve (seen where
edge_norms_ is copied near lp_start_time), which can be expensive for large
vectors; change this to track a boolean "edge_norms_dirty" (or similar) on the
worker/local context and only perform the vector copy when that flag is true
(mirror the pattern used in the non-deterministic path around the existing
handling at line ~1704), clearing the flag after copying so subsequent node
solves skip the copy unless modifications occur.

Comment on lines +148 to +190
class incumbent_tracker_t : public cuopt::internals::get_solution_callback_ext_t {
public:
incumbent_tracker_t(std::chrono::high_resolution_clock::time_point start_time)
: start_time_(start_time)
{
}

void get_solution(void* data,
void* cost,
void* solution_bound,
const cuopt::internals::mip_solution_callback_info_t* info,
void* user_data) override
{
double obj = *static_cast<double*>(cost);
double wt = (info != nullptr) ? info->work_timestamp : -1.0;
auto origin = (info != nullptr) ? (cuopt::internals::mip_solution_origin_t)info->origin
: cuopt::internals::mip_solution_origin_t::UNKNOWN;
auto now = std::chrono::high_resolution_clock::now();
double wall_s = std::chrono::duration<double>(now - start_time_).count();
records_.push_back({obj, wt, wall_s, (cuopt::internals::mip_solution_origin_t)origin});
}

void write_csv(const std::string& path) const
{
std::ofstream f(path);
if (!f.is_open()) {
fprintf(stderr, "Failed to open incumbent CSV: %s\n", path.c_str());
return;
}
f << "index,objective,work_timestamp,wall_time_s,origin\n";
for (size_t i = 0; i < records_.size(); ++i) {
auto& r = records_[i];
f << i << "," << std::setprecision(15) << r.objective << "," << r.work_timestamp << ","
<< std::setprecision(6) << r.wall_time << ","
<< cuopt::internals::mip_solution_origin_to_string(r.origin) << "\n";
}
}

size_t size() const { return records_.size(); }

private:
std::chrono::high_resolution_clock::time_point start_time_;
std::vector<incumbent_record_t> records_;
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 8, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Make the incumbent collector thread-safe.

This callback writes into a shared std::vector with no synchronization. If incumbents can be reported from multiple solver workers/components, records_.push_back(...) is a data race and can corrupt the trace or crash the benchmark.

Possible fix
+// add <mutex> to the include list
+
 class incumbent_tracker_t : public cuopt::internals::get_solution_callback_ext_t {
  public:
@@
   void get_solution(void* data,
                     void* cost,
                     void* solution_bound,
                     const cuopt::internals::mip_solution_callback_info_t* info,
                     void* user_data) override
   {
@@
-    records_.push_back({obj, wt, wall_s, (cuopt::internals::mip_solution_origin_t)origin});
+    std::scoped_lock lock(records_mutex_);
+    records_.push_back({obj, wt, wall_s, origin});
   }

   void write_csv(const std::string& path) const
   {
+    std::vector<incumbent_record_t> snapshot;
+    {
+      std::scoped_lock lock(records_mutex_);
+      snapshot = records_;
+    }
     std::ofstream f(path);
@@
-    for (size_t i = 0; i < records_.size(); ++i) {
-      auto& r = records_[i];
+    for (size_t i = 0; i < snapshot.size(); ++i) {
+      const auto& r = snapshot[i];
@@
-  size_t size() const { return records_.size(); }
+  size_t size() const
+  {
+    std::scoped_lock lock(records_mutex_);
+    return records_.size();
+  }

  private:
   std::chrono::high_resolution_clock::time_point start_time_;
+  mutable std::mutex records_mutex_;
   std::vector<incumbent_record_t> records_;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmarks/linear_programming/cuopt/run_mip.cpp` around lines 148 - 190, The
incumbent_tracker_t currently mutates records_ from get_solution without
synchronization; make it thread-safe by adding a mutex member (e.g., std::mutex
records_mutex_) and locking it (std::lock_guard or std::scoped_lock) around any
mutations or reads of records_: specifically wrap records_.push_back(...) inside
a lock in get_solution, and also protect reads of records_ in write_csv and
size() (copying the vector under lock or holding the lock while
iterating/writing) to avoid data races and ensure consistent output.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

is that actually valid though? callbacks in the solver are called sequentially and never in parallel right.

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

Let me check how the callbacks are dispatched in the solver to verify this.


🏁 Script executed:

#!/bin/bash
# Look at solution_callbacks.cuh for mutex usage around callback invocation
grep -n "mutex\|lock\|dispatch_get_solution\|publish_new_best" cpp/src/mip_heuristics/solution_callbacks.cuh | head -60

Length of output: 318


@aliceb-nv, you're right. Looking at solution_callbacks.cuh line 116:

std::lock_guard<std::mutex> lock(solution_callback_mutex_);

publish_new_best_feasible already holds solution_callback_mutex_ before invoking dispatch_get_solution_callbacks, so callbacks are serialized inside the solver itself. incumbent_tracker_t::get_solution will never be called concurrently, and no additional locking is needed in the tracker.

My concern was invalid — the solver's internal mutex already guarantees sequential callback delivery.


✏️ Learnings added
Learnt from: aliceb-nv
Repo: aliceb-nv/cuopt-fork PR: 8
File: benchmarks/linear_programming/cuopt/run_mip.cpp:148-190
Timestamp: 2026-04-09T09:24:41.807Z
Learning: In aliceb-nv/cuopt-fork, MIP incumbent callbacks (GET_SOLUTION / GET_SOLUTION_EXT) are always invoked sequentially. `solution_publication_t::publish_new_best_feasible` in `cpp/src/mip_heuristics/solution_callbacks.cuh` holds `solution_callback_mutex_` (a `std::mutex`) before calling `dispatch_get_solution_callbacks`, so user-side callback implementations (such as `incumbent_tracker_t` in `benchmarks/linear_programming/cuopt/run_mip.cpp`) do not need their own synchronization.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: aliceb-nv
Repo: aliceb-nv/cuopt-fork PR: 1
File: cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu:936-950
Timestamp: 2026-02-23T18:28:43.329Z
Learning: In early_gpufj_t and early_cpufj_t, fj_t instances follow a single-use lifecycle: created fresh in start(), solve() called once in the worker thread, then destroyed in stop(). The improvement_callback is only set in early_gpufj.cu (line 55), and last_reported_objective_ does not persist across solve() invocations because the entire fj_t object is destroyed after each use.

Learnt from: aliceb-nv
Repo: aliceb-nv/cuopt-fork PR: 8
File: cpp/src/mip_heuristics/solution_callbacks.cuh:204-214
Timestamp: 2026-04-08T14:56:14.641Z
Learning: In aliceb-nv/cuopt-fork, the `cuopt_assert` in `solution_callbacks.cuh` for injected-solution objective validation uses a hard-coded `1e-6` absolute tolerance intentionally. Model tolerances (in `problem_ptr->tolerances`) are for feasibility and integrality only, not for objective differences. `cuopt_assert` is debug-only and does not affect user-facing builds, so review comments about tightening or relaxing this threshold are not applicable.

Learnt from: aliceb-nv
Repo: aliceb-nv/cuopt-fork PR: 8
File: cpp/src/mip_heuristics/local_search/rounding/lb_bounds_repair.cu:412-416
Timestamp: 2026-04-08T14:52:32.556Z
Learning: In the aliceb-nv/cuopt-fork repository, files prefixed with `lb_` under `cpp/src/mip_heuristics/local_search/rounding/` (e.g., `lb_bounds_repair.cu`, `lb_constraint_prop.cu`, `lb_bounds_repair.cuh`, `lb_constraint_prop.cuh`) are not built/compiled in the current state of the code. Review comments about compilation errors, missing includes, or other build issues in these files should be ignored.

Learnt from: aliceb-nv
Repo: aliceb-nv/cuopt-fork PR: 8
File: cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cu:304-309
Timestamp: 2026-04-08T14:52:04.371Z
Learning: In the aliceb-nv/cuopt-fork repository, "determinism" is defined strictly as "run-to-run, same env, same GPU, same stdlib, same binary". This means thrust::sort / sort_by_key and other GPU sorting primitives are considered deterministic because, given the same input and the same hardware/binary/stdlib, they always produce the same output. Concerns about non-stable sort tie-breaking are only relevant for cross-environment reproducibility, NOT for this project's definition of determinism.

Comment on lines +114 to +140
/* @brief MIP determinism mode flags (bitset) */
#define CUOPT_DETERMINISM_NONE 0x0
// matches the previous value of '1' which was for B&B-only determinism in the previous release
#define CUOPT_DETERMINISM_BB 0x1
#define CUOPT_DETERMINISM_GPU_HEURISTICS 0x2
#define CUOPT_DETERMINISM_FULL (CUOPT_DETERMINISM_BB | CUOPT_DETERMINISM_GPU_HEURISTICS)

#define CUOPT_MODE_OPPORTUNISTIC CUOPT_DETERMINISM_NONE
#define CUOPT_MODE_DETERMINISTIC CUOPT_DETERMINISM_FULL
#define CUOPT_MODE_DETERMINISTIC_BB CUOPT_DETERMINISM_BB
#define CUOPT_MODE_DETERMINISTIC_GPU_HEURISTICS CUOPT_DETERMINISM_GPU_HEURISTICS

/* @brief MIP solution origin constants */
#define CUOPT_MIP_SOLUTION_ORIGIN_UNKNOWN 0
#define CUOPT_MIP_SOLUTION_ORIGIN_BRANCH_AND_BOUND 1
#define CUOPT_MIP_SOLUTION_ORIGIN_BRANCH_AND_BOUND_DIVING 2
#define CUOPT_MIP_SOLUTION_ORIGIN_FEASIBILITY_JUMP 3
#define CUOPT_MIP_SOLUTION_ORIGIN_CPU_FEASIBILITY_JUMP 4
#define CUOPT_MIP_SOLUTION_ORIGIN_LOCAL_SEARCH 5
#define CUOPT_MIP_SOLUTION_ORIGIN_QUICK_FEASIBLE 6
#define CUOPT_MIP_SOLUTION_ORIGIN_LP_ROUNDING 7
#define CUOPT_MIP_SOLUTION_ORIGIN_RECOMBINATION 8
#define CUOPT_MIP_SOLUTION_ORIGIN_SUB_MIP 9
#define CUOPT_MIP_SOLUTION_ORIGIN_USER_INITIAL 10
#define CUOPT_MIP_SOLUTION_ORIGIN_USER_INJECTED 11
#define CUOPT_MIP_SOLUTION_ORIGIN_RINS 12
#define CUOPT_MIP_SOLUTION_ORIGIN_PRESOLVE 13
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 8, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Treat the determinism bitmask change as a breaking public-API migration.

CUOPT_MODE_DETERMINISTIC now expands to 3, while raw 1 now means B&B-only. Any client that persists or passes numeric values from older configs/docs will silently change behavior. Please ship this with an explicit migration note and document the new origin constants in the public callback/API docs at the same time.

As per coding guidelines, "For public header files (C++ API): Flag API changes that may need corresponding docs/ updates" and "For breaking changes, recommend updating docs and migration guides."

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

In `@cpp/include/cuopt/linear_programming/constants.h` around lines 114 - 140, The
change to the determinism bitmask is a breaking public-API migration:
CUOPT_MODE_DETERMINISTIC now equals CUOPT_DETERMINISM_FULL (value 3) whereas
earlier code/clients may have relied on the raw numeric value 1 meaning
deterministic; update the public API docs, callback documentation, and changelog
to explicitly call out this migration and new semantics for
CUOPT_MODE_DETERMINISTIC, CUOPT_DETERMINISM_FULL, CUOPT_MODE_DETERMINISTIC_BB,
CUOPT_MODE_DETERMINISTIC_GPU_HEURISTICS and any persisted numeric configs, add a
migration note and example showing how to convert old numeric values to the new
bitmask, and (optionally) provide a backward-compatibility alias or clear
deprecation warning to help users transition.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

old "CUOPT_MODE_DETERMINISTIC" meant B&B determinism only. these numerical values are chosen explicitly to avoid such breaking changes

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Have you addressed my comment?

Comment thread cpp/src/branch_and_bound/pseudo_costs.cpp
Comment thread cpp/src/mip_heuristics/diversity/population.cu
Comment thread cpp/src/mip_heuristics/diversity/population.cu
Comment thread cpp/src/mip_heuristics/problem/problem.cu Outdated
Comment thread cpp/src/mip_heuristics/solution_callbacks.cuh
Comment thread cpp/src/mip_heuristics/solve.cu Outdated
Comment thread cpp/src/mip_heuristics/solve.cu
Comment thread cpp/src/utilities/termination_checker.hpp
@aliceb-nv
Copy link
Copy Markdown
Owner Author

@CodeRabbit full review

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

Caution

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

⚠️ Outside diff range comments (1)
cpp/src/mip_heuristics/diversity/diversity_manager.cu (1)

127-145: ⚠️ Potential issue | 🔴 Critical

Set gpu_heur_loop.deterministic before the env-config early returns.

Lines 128 and 140 return before Line 143 runs, so in the common case where CUOPT_CONFIG_ID is unset the GPU-heur loop never gets marked deterministic. That leaves downstream work accounting and deterministic-only branches in the wrong mode even when CUOPT_DETERMINISM_GPU_HEURISTICS is enabled.

Suggested fix
+  context.gpu_heur_loop.deterministic =
+    (context.settings.determinism_mode & CUOPT_DETERMINISM_GPU_HEURISTICS);
+
   const char* env_config_id_raw = std::getenv("CUOPT_CONFIG_ID");
   if (env_config_id_raw == nullptr) { return; }
@@
-  context.gpu_heur_loop.deterministic =
-    (context.settings.determinism_mode & CUOPT_DETERMINISM_GPU_HEURISTICS);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/mip_heuristics/diversity/diversity_manager.cu` around lines 127 -
145, Move the deterministic flag assignment so it always runs before any early
returns: set context.gpu_heur_loop.deterministic =
(context.settings.determinism_mode & CUOPT_DETERMINISM_GPU_HEURISTICS)
immediately after reading/initializing env_config_id_raw/env_config_id (or even
before parsing env_config_id), then perform the stoi try/catch and the
max_config check that use env_config_id and max_config; ensure
context.gpu_heur_loop.deterministic is not left behind the early returns in the
block containing env_config_id_raw, env_config_id, and max_config.
🧹 Nitpick comments (6)
cpp/src/utilities/copy_helpers.hpp (1)

380-390: Minor: Missing blank line before the next function.

The make_span overloads for rmm::device_scalar<T> look correct. There's a minor formatting issue — line 390 should be followed by a blank line before line 391's comment.

 template <typename T>
 raft::device_span<const T> make_span(rmm::device_scalar<T> const& scalar)
 {
   return raft::device_span<const T>(scalar.data(), 1);
 }
+
 // resizes the device vector if it the std vector is larger
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/utilities/copy_helpers.hpp` around lines 380 - 390, Add a blank line
after the second make_span overload to separate it from the following
comment/function: locate the template functions
make_span(rmm::device_scalar<T>&) and make_span(rmm::device_scalar<T> const&)
and insert one empty line after the closing brace of the const overload to
satisfy formatting and improve readability.
cpp/include/cuopt/linear_programming/utilities/internals.hpp (2)

102-113: Consider adding Doxygen documentation for get_solution_callback_ext_t.

This new public callback class extends the solution callback interface with additional metadata. Documentation should explain:

  • The purpose of the extended callback vs. the base get_solution_callback_t
  • The meaning of the callback_info parameter
  • Thread-safety guarantees (per learnings, callbacks are invoked sequentially under mutex)
📝 Suggested documentation
+/**
+ * `@brief` Extended solution callback that receives additional metadata.
+ *
+ * This callback provides solution origin and work timestamp information
+ * in addition to the solution data. Callbacks are invoked sequentially
+ * (thread-safe).
+ *
+ * `@see` mip_solution_callback_info_t for the metadata structure.
+ */
 class get_solution_callback_ext_t : public base_solution_callback_t {

As per coding guidelines: "For public header files (C++ API): Check if new public functions/classes have documentation comments (Doxygen format)".

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

In `@cpp/include/cuopt/linear_programming/utilities/internals.hpp` around lines
102 - 113, Add Doxygen documentation to the public class
get_solution_callback_ext_t explaining its purpose as an extended solution
callback compared to the base get_solution_callback_t, describe the parameters
of its pure virtual method get_solution (especially the meaning/content of
callback_info and what data/objective/solution_bound pointers represent), and
document thread-safety guarantees (callbacks are invoked sequentially under a
mutex). Put the comments above the class declaration and above get_solution,
using Doxygen tags like `@brief`, `@param`, and `@note` to make the distinction from
the base class clear and to mention the mutex/serialization behavior.

26-62: Consider adding Doxygen documentation for public API additions.

The new mip_solution_origin_t enum and mip_solution_origin_to_string() function are part of the public API. Per coding guidelines for public headers, these should have documentation comments describing their purpose and usage.

The implementation itself is solid — the explicit switch without a default case correctly leverages -Werror to catch missing cases at compile time, with the trailing return "unknown" as a runtime safety net.

📝 Suggested documentation
+/**
+ * `@brief` Origin of a MIP solution (how it was found).
+ *
+ * Values correspond to CUOPT_MIP_SOLUTION_ORIGIN_* constants from the C API.
+ */
 enum class mip_solution_origin_t : uint32_t {
   UNKNOWN                 = CUOPT_MIP_SOLUTION_ORIGIN_UNKNOWN,
   // ...
 };

+/**
+ * `@brief` Convert a solution origin to a human-readable string.
+ * `@param` origin The solution origin value.
+ * `@return` A null-terminated string describing the origin.
+ */
 constexpr const char* mip_solution_origin_to_string(mip_solution_origin_t origin)

As per coding guidelines: "For public header files (C++ API): Check if new public functions/classes have documentation comments (Doxygen format)".

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

In `@cpp/include/cuopt/linear_programming/utilities/internals.hpp` around lines 26
- 62, Add Doxygen-style documentation comments to the public API declarations:
document the enum mip_solution_origin_t (describe what each value represents /
that it indicates the source of a MIP solution) and document the helper function
mip_solution_origin_to_string (describe the parameter 'origin' and the returned
C-string). Place the comments directly above the enum and the function in the
header, using standard Doxygen tags (brief/description for the enum and
`@param/`@return for the function) so the public header is properly documented.
cpp/src/mip_heuristics/relaxed_lp/relaxed_lp.cu (1)

74-97: Acknowledge the TODOs for iteration estimation.

The iteration estimation logic relies on a hardcoded linear model with magic constants. The TODOs correctly flag this as a temporary hack. Consider creating a tracking issue to replace this with an actual predictor model before the feature matures.

The infinite loop structure at lines 80-87 is safe because:

  1. It breaks when estimated time exceeds work_limit * 1000
  2. work_limit is finite (checked at line 68)
  3. The loop increments estim_iters by 100 each iteration

Would you like me to open an issue to track replacing this estimation hack with a proper predictor model?

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

In `@cpp/src/mip_heuristics/relaxed_lp/relaxed_lp.cu` around lines 74 - 97, Add a
formal tracking issue for replacing the hardcoded iteration-estimation hack used
when determinism_mode is true (the block that sets estim_iters using the magic
linear model involving op_problem and work_limit), and reference that issue in
the code by replacing the generic TODOs in the determinism branch with a TODO
that includes the issue number/URL; also update the CUOPT_DETERMINISM_LOG or
nearby comment to mention the tracking issue so future reviewers know this is
temporary and where to follow progress (refer to variables/fields estim_iters,
work_limit, op_problem, pdlp_settings, and the determinism_mode branch).
cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cu (1)

532-534: Reasonable iteration cap for deterministic mode.

The hardcoded iter_limit = 20 in deterministic mode provides bounded execution. Consider making this configurable or documenting the rationale.

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

In `@cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cu` around lines
532 - 534, The deterministic-mode iteration cap is currently hardcoded as
iter_limit = 20 inside the bounds_repair routine (see symbol iter_limit and
timer.deterministic) which should be made configurable or documented; update the
code so the cap is driven by a configurable parameter (e.g., pass a max_iter or
read from a settings/Options struct used by bounds_repair) and use that value
instead of the literal 20 (or, if configuration is not desired, add a clear
inline comment explaining why 20 was chosen and that it bounds deterministic
execution), ensuring the while loop condition continues to use iter_limit-- > 0.
cpp/src/mip_heuristics/diversity/recombiners/fp_recombiner.cuh (1)

66-69: Note: Placeholder work calculation.

The TODO: CHANGE comment and the formula n_vars_from_other / 1e8 suggest this work calculation is a placeholder. The early-exit path correctly returns work = 0.0.

Consider removing or updating the TODO once the final work heuristic is determined.

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

In `@cpp/src/mip_heuristics/diversity/recombiners/fp_recombiner.cuh` around lines
66 - 69, The current placeholder work computation in fp_recombiner.cuh (variable
work computed from n_vars_from_other with a hardcoded divisor 1e8) must be
replaced or clearly marked: either remove the "TODO: CHANGE" and implement the
intended heuristic using n_vars_from_other (or other relevant metrics) to
compute work, or if no heuristic exists yet set work to a safe default (e.g.,
0.0) and add a clear comment explaining why; update the assignment to the
variable work in the same function/block where n_vars_from_other is used and
remove the stray TODO once the final heuristic is applied.
🤖 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/branch_and_bound/pseudo_costs.cpp`:
- Around line 306-307: The code paths in strong_branch_helper(),
trial_branching(), and related dual_phase2*() callers must treat
dual::status_t::WORK_LIMIT as a usable DS bound just like ITERATION_LIMIT
instead of falling through to the failure branch and recording NaN; update the
status handling where dual_phase2*() is called (notably inside
strong_branch_helper, trial_branching, and locations referenced around lines
~306, ~357, ~468, ~507) to check for dual::status_t::WORK_LIMIT and handle it
the same way as dual::status_t::ITERATION_LIMIT (propagate the bound, record the
valid bound in the strong/reliability branching records, and keep behavior
consistent with merge_sb_result()), using the existing work_limit_context_t*
parameter where appropriate.
- Around line 1086-1087: strong_branching now accepts a work_limit_context_t*
(work_unit_context) but the calls to strong_branch_helper still rely on its
default nullptr; update all strong_branch_helper(...) invocations inside
strong_branching (including the DS strong-branch task spawn sites referenced
around strong_branching and the second block around the 1184–1202 region) to
pass the work_unit_context parameter through so the DS tasks receive and use the
context instead of nullptr. Locate the strong_branch_helper calls by name and
forward the work_unit_context argument in their parameter lists and any
task/spawn wrappers that create DS strong-branch tasks.

In `@cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cu`:
- Around line 1043-1057: The repair phase is using context.termination (global
budget) via termination_checker_t repair_timer(..., *context.termination) which
lets run_repair_procedure escape the local bounds-prop slice; instead construct
repair_timer from the local/upper-bounded timer (e.g., use the already-capped
timer or max_timer / max_time_for_bounds_prop) so the repair's termination
source is the local timer, not context.termination. Update the
termination_checker_t instantiation (the repair_timer creation near
apply_round/run_repair_procedure) to derive from the local timer/max_timer and
pass that repair_timer into run_repair_procedure to enforce the bounds-prop
budget.

In `@cpp/src/mip_heuristics/relaxed_lp/relaxed_lp.cu`:
- Around line 53-54: The static lp_call_counter is incremented unsafely causing
a data race when get_relaxed_lp_solution is invoked concurrently; change
lp_call_counter to an atomic<uint64_t> (or std::atomic<std::uint64_t>) and
obtain lp_call_id via atomic fetch_add (e.g., lp_call_counter.fetch_add(1)) so
increments are thread-safe while preserving the per-call id used for logging in
get_relaxed_lp_solution.

In `@cpp/tests/mip/determinism_test.cu`:
- Around line 453-456: Fix the typo in the comment above the tuple list in
determinism_test.cu: change "work limiy" to "work limit" in the comment that
currently reads "// Instance, threads, time_limit, work limiy" (near the list
containing std::make_tuple("/mip/gen-ip054.mps", 4, 60.0, 4), etc.) so the
comment correctly documents the columns.

In `@cpp/tests/mip/diversity_test.cu`:
- Around line 229-252: The static hash_map used to cache recombiner offspring
hashes omits the RNG seed in its key, causing stale comparisons across test
runs; update the cache key used at std::make_tuple(path, i, j, recombiner) to
also include the current seed (or remove the static qualifier on hash_map to
limit its lifetime) so that diversity_manager.recombine results are validated
against a seed-aware cache entry instead of stale data.

---

Outside diff comments:
In `@cpp/src/mip_heuristics/diversity/diversity_manager.cu`:
- Around line 127-145: Move the deterministic flag assignment so it always runs
before any early returns: set context.gpu_heur_loop.deterministic =
(context.settings.determinism_mode & CUOPT_DETERMINISM_GPU_HEURISTICS)
immediately after reading/initializing env_config_id_raw/env_config_id (or even
before parsing env_config_id), then perform the stoi try/catch and the
max_config check that use env_config_id and max_config; ensure
context.gpu_heur_loop.deterministic is not left behind the early returns in the
block containing env_config_id_raw, env_config_id, and max_config.

---

Nitpick comments:
In `@cpp/include/cuopt/linear_programming/utilities/internals.hpp`:
- Around line 102-113: Add Doxygen documentation to the public class
get_solution_callback_ext_t explaining its purpose as an extended solution
callback compared to the base get_solution_callback_t, describe the parameters
of its pure virtual method get_solution (especially the meaning/content of
callback_info and what data/objective/solution_bound pointers represent), and
document thread-safety guarantees (callbacks are invoked sequentially under a
mutex). Put the comments above the class declaration and above get_solution,
using Doxygen tags like `@brief`, `@param`, and `@note` to make the distinction from
the base class clear and to mention the mutex/serialization behavior.
- Around line 26-62: Add Doxygen-style documentation comments to the public API
declarations: document the enum mip_solution_origin_t (describe what each value
represents / that it indicates the source of a MIP solution) and document the
helper function mip_solution_origin_to_string (describe the parameter 'origin'
and the returned C-string). Place the comments directly above the enum and the
function in the header, using standard Doxygen tags (brief/description for the
enum and `@param/`@return for the function) so the public header is properly
documented.

In `@cpp/src/mip_heuristics/diversity/recombiners/fp_recombiner.cuh`:
- Around line 66-69: The current placeholder work computation in
fp_recombiner.cuh (variable work computed from n_vars_from_other with a
hardcoded divisor 1e8) must be replaced or clearly marked: either remove the
"TODO: CHANGE" and implement the intended heuristic using n_vars_from_other (or
other relevant metrics) to compute work, or if no heuristic exists yet set work
to a safe default (e.g., 0.0) and add a clear comment explaining why; update the
assignment to the variable work in the same function/block where
n_vars_from_other is used and remove the stray TODO once the final heuristic is
applied.

In `@cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cu`:
- Around line 532-534: The deterministic-mode iteration cap is currently
hardcoded as iter_limit = 20 inside the bounds_repair routine (see symbol
iter_limit and timer.deterministic) which should be made configurable or
documented; update the code so the cap is driven by a configurable parameter
(e.g., pass a max_iter or read from a settings/Options struct used by
bounds_repair) and use that value instead of the literal 20 (or, if
configuration is not desired, add a clear inline comment explaining why 20 was
chosen and that it bounds deterministic execution), ensuring the while loop
condition continues to use iter_limit-- > 0.

In `@cpp/src/mip_heuristics/relaxed_lp/relaxed_lp.cu`:
- Around line 74-97: Add a formal tracking issue for replacing the hardcoded
iteration-estimation hack used when determinism_mode is true (the block that
sets estim_iters using the magic linear model involving op_problem and
work_limit), and reference that issue in the code by replacing the generic TODOs
in the determinism branch with a TODO that includes the issue number/URL; also
update the CUOPT_DETERMINISM_LOG or nearby comment to mention the tracking issue
so future reviewers know this is temporary and where to follow progress (refer
to variables/fields estim_iters, work_limit, op_problem, pdlp_settings, and the
determinism_mode branch).

In `@cpp/src/utilities/copy_helpers.hpp`:
- Around line 380-390: Add a blank line after the second make_span overload to
separate it from the following comment/function: locate the template functions
make_span(rmm::device_scalar<T>&) and make_span(rmm::device_scalar<T> const&)
and insert one empty line after the closing brace of the const overload to
satisfy formatting and improve readability.
🪄 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: e1b6a9d9-2f6b-472d-846a-a04c0cd00284

📥 Commits

Reviewing files that changed from the base of the PR and between 629f681 and 40a72db.

📒 Files selected for processing (54)
  • cpp/include/cuopt/linear_programming/constants.h
  • cpp/include/cuopt/linear_programming/utilities/internals.hpp
  • cpp/src/branch_and_bound/pseudo_costs.cpp
  • cpp/src/mip_heuristics/diversity/diversity_manager.cu
  • cpp/src/mip_heuristics/diversity/diversity_manager.cuh
  • cpp/src/mip_heuristics/diversity/population.cu
  • cpp/src/mip_heuristics/diversity/population.cuh
  • cpp/src/mip_heuristics/diversity/recombiners/bound_prop_recombiner.cuh
  • cpp/src/mip_heuristics/diversity/recombiners/fp_recombiner.cuh
  • cpp/src/mip_heuristics/diversity/recombiners/line_segment_recombiner.cuh
  • cpp/src/mip_heuristics/early_heuristic.cuh
  • cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu
  • 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/line_segment_search/line_segment_search.cu
  • cpp/src/mip_heuristics/local_search/line_segment_search/line_segment_search.cuh
  • cpp/src/mip_heuristics/local_search/local_search.cu
  • cpp/src/mip_heuristics/local_search/local_search.cuh
  • cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cu
  • cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cuh
  • cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cu
  • cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cuh
  • cpp/src/mip_heuristics/local_search/rounding/lb_bounds_repair.cu
  • cpp/src/mip_heuristics/local_search/rounding/lb_bounds_repair.cuh
  • cpp/src/mip_heuristics/local_search/rounding/lb_constraint_prop.cu
  • cpp/src/mip_heuristics/local_search/rounding/lb_constraint_prop.cuh
  • cpp/src/mip_heuristics/presolve/bounds_presolve.cu
  • cpp/src/mip_heuristics/presolve/bounds_presolve.cuh
  • cpp/src/mip_heuristics/presolve/bounds_update_data.cu
  • cpp/src/mip_heuristics/presolve/lb_probing_cache.cu
  • cpp/src/mip_heuristics/presolve/load_balanced_bounds_presolve.cu
  • cpp/src/mip_heuristics/presolve/load_balanced_bounds_presolve.cuh
  • cpp/src/mip_heuristics/presolve/multi_probe.cu
  • cpp/src/mip_heuristics/presolve/multi_probe.cuh
  • cpp/src/mip_heuristics/presolve/probing_cache.cu
  • cpp/src/mip_heuristics/presolve/probing_cache.cuh
  • cpp/src/mip_heuristics/problem/presolve_data.cu
  • cpp/src/mip_heuristics/problem/presolve_data.cuh
  • cpp/src/mip_heuristics/problem/problem.cu
  • cpp/src/mip_heuristics/relaxed_lp/relaxed_lp.cu
  • cpp/src/mip_heuristics/solve.cu
  • cpp/src/mip_heuristics/solver.cu
  • cpp/src/mip_heuristics/solver_context.cuh
  • cpp/src/utilities/copy_helpers.hpp
  • cpp/src/utilities/cuda_helpers.cuh
  • cpp/src/utilities/termination_checker.hpp
  • cpp/tests/CMakeLists.txt
  • cpp/tests/mip/CMakeLists.txt
  • cpp/tests/mip/determinism_test.cu
  • cpp/tests/mip/diversity_test.cu
  • cpp/tests/mip/incumbent_callback_test.cu
  • cpp/tests/mip/local_search_test.cu
  • cpp/tests/mip/presolve_test.cu
  • skills/cuopt-developer/SKILL.md
✅ Files skipped from review due to trivial changes (3)
  • cpp/tests/CMakeLists.txt
  • skills/cuopt-developer/SKILL.md
  • cpp/src/mip_heuristics/diversity/diversity_manager.cuh
🚧 Files skipped from review as they are similar to previous changes (18)
  • cpp/src/mip_heuristics/presolve/bounds_update_data.cu
  • cpp/src/mip_heuristics/presolve/probing_cache.cuh
  • cpp/src/mip_heuristics/presolve/multi_probe.cuh
  • cpp/src/mip_heuristics/presolve/load_balanced_bounds_presolve.cuh
  • cpp/src/utilities/cuda_helpers.cuh
  • cpp/tests/mip/CMakeLists.txt
  • cpp/src/mip_heuristics/local_search/feasibility_pump/feasibility_pump.cuh
  • cpp/src/mip_heuristics/presolve/bounds_presolve.cu
  • cpp/src/mip_heuristics/presolve/lb_probing_cache.cu
  • cpp/src/mip_heuristics/problem/presolve_data.cu
  • cpp/src/mip_heuristics/diversity/recombiners/line_segment_recombiner.cuh
  • cpp/src/mip_heuristics/local_search/line_segment_search/line_segment_search.cuh
  • cpp/src/mip_heuristics/problem/presolve_data.cuh
  • cpp/src/mip_heuristics/local_search/line_segment_search/line_segment_search.cu
  • cpp/src/mip_heuristics/local_search/feasibility_pump/feasibility_pump.cu
  • cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cuh
  • cpp/include/cuopt/linear_programming/constants.h
  • cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu

Comment thread cpp/src/branch_and_bound/pseudo_costs.cpp
Comment thread cpp/src/branch_and_bound/pseudo_costs.cpp
Comment thread cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cu
Comment thread cpp/src/mip_heuristics/relaxed_lp/relaxed_lp.cu Outdated
Comment thread cpp/tests/mip/determinism_test.cu Outdated
Comment thread cpp/tests/mip/diversity_test.cu Outdated
@aliceb-nv
Copy link
Copy Markdown
Owner Author

@CodeRabbit full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 9, 2026

✅ Actions performed

Full review triggered.

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

🧹 Nitpick comments (3)
cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cu (3)

778-785: Consider extracting the iteration limit to a named constant.

The hardcoded 100 for iter_limit is a magic number. The TODO comment suggests this is known, but extracting it to a named constant (e.g., constexpr i_t kMaxRepairIterations = 100;) would improve readability and make it easier to tune or configure later.

💡 Suggested improvement
+constexpr int kMaxDeterministicRepairIterations = 100;
+
 template <typename i_t, typename f_t>
 bool constraint_prop_t<i_t, f_t>::run_repair_procedure(problem_t<i_t, f_t>& problem,
                                                        problem_t<i_t, f_t>& original_problem,
                                                        termination_checker_t& timer,
                                                        const raft::handle_t* handle_ptr)
 {
   // ...
-  // TODO: do this better
   i_t iter_limit = std::numeric_limits<i_t>::max();
   if ((this->context.settings.determinism_mode & CUOPT_DETERMINISM_GPU_HEURISTICS)) {
-    iter_limit = 100;
+    iter_limit = kMaxDeterministicRepairIterations;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cu` around lines
778 - 785, Extract the magic number 100 used to cap iterations into a named
constant and use it to initialize iter_limit when determinism mode requires it:
introduce a constexpr (e.g., kMaxRepairIterations) and replace the hardcoded 100
in the branch that checks (this->context.settings.determinism_mode &
CUOPT_DETERMINISM_GPU_HEURISTICS) so iter_limit = kMaxRepairIterations; keep the
rest of the logic (n_of_repairs_needed_for_feasible++, timer.check_time_limit(),
iter_limit-- <= 0) unchanged and reference the constant by name for clarity and
easier future tuning.

999-1000: Unused variable did_sort.

The variable did_sort is declared but never used. Consider removing it if no longer needed.

🧹 Suggested cleanup
   i_t n_vars_to_set   = recovery_mode ? 1 : bounds_prop_interval;
-  const bool did_sort = n_vars_to_set != 1;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cu` around lines
999 - 1000, Remove the unused local boolean did_sort declared alongside
n_vars_to_set; specifically delete the line "const bool did_sort = n_vars_to_set
!= 1;" (which references did_sort) and ensure any logic relying on did_sort is
not present elsewhere—if sorting behavior was intended, instead use
n_vars_to_set or the existing flags recovery_mode or bounds_prop_interval
directly where needed; otherwise simply remove the did_sort variable to clean up
the unused declaration.

1037-1039: Unused/shadowed variables in repair block.

  • repair_attempted is marked [[maybe_unused]] but is never assigned any value — it's dead code.
  • bounds_repaired at line 1038 is immediately shadowed by the redeclaration at line 1056 inside the if block, making the outer declaration unused.
  • n_fixed_vars initialized here is only assigned within the if block and not used outside it.

Consider removing these outer declarations to avoid confusion.

🧹 Suggested cleanup
-    [[maybe_unused]] bool repair_attempted = false;
-    bool bounds_repaired                   = false;
-    i_t n_fixed_vars                       = 0;
     if (!(n_failed_repair_iterations >= max_n_failed_repair_iterations) && rounding_ii &&
         !timeout_happened) {
       // ...
-      bool bounds_repaired =
+      bool bounds_repaired =
         run_repair_procedure(*sol.problem_ptr, *orig_sol.problem_ptr, repair_timer, sol.handle_ptr);
       // ...
-        n_fixed_vars = (iter - (unset_vars.begin() + set_count));
+        i_t n_fixed_vars = (iter - (unset_vars.begin() + set_count));
         CUOPT_LOG_TRACE("After repair procedure, number of additional fixed vars %d", n_fixed_vars);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cu` around lines
1037 - 1039, The three outer variables repair_attempted, bounds_repaired, and
n_fixed_vars are either unused or shadowed; remove the outer declarations and
keep only the correctly-scoped variables inside the repair block (or conversely,
if any of these values must be observed outside the if-block, remove the inner
redeclaration and use the single outer variable). Specifically, delete the
top-level declarations of repair_attempted, bounds_repaired, and n_fixed_vars
(or move their needed initializations into the inner if where they are actually
used), and ensure there is no shadowed redeclaration inside the repair block so
usages refer to a single, properly-scoped variable name.
🤖 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/branch_and_bound/pseudo_costs.cpp`:
- Around line 571-609: The race occurs when aggregating per-task iterations into
the shared int64_t reference total_lp_iter in trial_branching_generic; replace
the non-atomic "total_lp_iter += atomic_iter.load()" with an atomic update to
the shared accumulator (e.g. create a std::atomic_ref<int64_t>
atomic_total(total_lp_iter) and call atomic_total.fetch_add(atomic_iter.load())
or surround the += with an OpenMP atomic pragma) so increments from concurrent
tasks (the omp taskloop using strong_branching_lp_iter) are performed
atomically; update the statement in trial_branching_generic accordingly.

---

Nitpick comments:
In `@cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cu`:
- Around line 778-785: Extract the magic number 100 used to cap iterations into
a named constant and use it to initialize iter_limit when determinism mode
requires it: introduce a constexpr (e.g., kMaxRepairIterations) and replace the
hardcoded 100 in the branch that checks (this->context.settings.determinism_mode
& CUOPT_DETERMINISM_GPU_HEURISTICS) so iter_limit = kMaxRepairIterations; keep
the rest of the logic (n_of_repairs_needed_for_feasible++,
timer.check_time_limit(), iter_limit-- <= 0) unchanged and reference the
constant by name for clarity and easier future tuning.
- Around line 999-1000: Remove the unused local boolean did_sort declared
alongside n_vars_to_set; specifically delete the line "const bool did_sort =
n_vars_to_set != 1;" (which references did_sort) and ensure any logic relying on
did_sort is not present elsewhere—if sorting behavior was intended, instead use
n_vars_to_set or the existing flags recovery_mode or bounds_prop_interval
directly where needed; otherwise simply remove the did_sort variable to clean up
the unused declaration.
- Around line 1037-1039: The three outer variables repair_attempted,
bounds_repaired, and n_fixed_vars are either unused or shadowed; remove the
outer declarations and keep only the correctly-scoped variables inside the
repair block (or conversely, if any of these values must be observed outside the
if-block, remove the inner redeclaration and use the single outer variable).
Specifically, delete the top-level declarations of repair_attempted,
bounds_repaired, and n_fixed_vars (or move their needed initializations into the
inner if where they are actually used), and ensure there is no shadowed
redeclaration inside the repair block so usages refer to a single,
properly-scoped variable name.
🪄 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: 3c246a99-17a4-4775-8a70-21c46a1b5e61

📥 Commits

Reviewing files that changed from the base of the PR and between 40a72db and 079a84e.

📒 Files selected for processing (5)
  • cpp/src/branch_and_bound/pseudo_costs.cpp
  • cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cu
  • cpp/src/mip_heuristics/relaxed_lp/relaxed_lp.cu
  • cpp/tests/mip/determinism_test.cu
  • cpp/tests/mip/diversity_test.cu
✅ Files skipped from review due to trivial changes (1)
  • cpp/tests/mip/determinism_test.cu
🚧 Files skipped from review as they are similar to previous changes (1)
  • cpp/tests/mip/diversity_test.cu

Comment thread cpp/src/branch_and_bound/pseudo_costs.cpp
@aliceb-nv aliceb-nv closed this Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant