Skip to content

Commit 917f577

Browse files
authored
Mod-2 cut separator for zero-half cuts (#1726)
- Add general mod-2 zero-half separation. The separator finds combinations of constraint rows whose odd coefficients cancel modulo 2 while their aggregated right-hand side remains odd. Halving and rounding this aggregate yields a valid cut. - Use sparse GF(2) elimination to find these row dependencies, then transform, lift, score, and add violated candidates to the cut pool. - Propagate B&B timeouts to active node solves and improve work unit checks for some cuts. - Update PaPILO pin for the coefficient-capacity guard fix. On H100, two runs each: Main vs mod-2 **Feasible** | 224.0 | 226.5 | +2.5 **Average error gap** | 12.285 | 11.655 | -0.630 **Optimal instances** | 76.5 | 77.5 | +1.0 **Root gap closed average** | 29.7518% | 31.0948% | +1.3429% **Root gap closed shifted geomean** (+1) | 10.4875% | 11.1626% | +0.6750% MIP gap unchanged, due to some cut passes taking longer. Authors: - Akif ÇÖRDÜK (https://github.com/akifcorduk) Approvers: - Alice Boucher (https://github.com/aliceb-nv) - Nicolas L. Guidotti (https://github.com/nguidotti) - Ramakrishna Prabhu (https://github.com/ramakrishnap-nv) URL: #1726
1 parent a809fc6 commit 917f577

9 files changed

Lines changed: 1011 additions & 45 deletions

File tree

cpp/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ FetchContent_Declare(
277277
# This is the reason we are using the development branch
278278
# from Oct 12, 2025. Once these changes are merged into the main branch,
279279
#we can switch to the main branch.
280-
GIT_TAG "32b3a87dbf4955d5a2803be74145c389ea31434d"
280+
GIT_TAG "55d5edece584885061639ecf3a6eb8a4629be9f2"
281281
GIT_PROGRESS TRUE
282282
EXCLUDE_FROM_ALL
283283
SYSTEM

cpp/src/branch_and_bound/branch_and_bound.cpp

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1565,8 +1565,9 @@ dual_status_t branch_and_bound_t<i_t, f_t>::solve_node_lp(
15651565
} else {
15661566
lp_settings.cut_off = cutoff + settings_.dual_tol;
15671567
}
1568-
lp_settings.inside_mip = 2;
1569-
lp_settings.time_limit = settings_.time_limit - toc(exploration_stats_.start_time);
1568+
lp_settings.inside_mip = 2;
1569+
lp_settings.time_limit = settings_.time_limit - toc(exploration_stats_.start_time);
1570+
if (lp_settings.time_limit <= 0.0) { return dual_status_t::TIME_LIMIT; }
15701571
lp_settings.scale_columns = false;
15711572
lp_settings.iteration_limit = iter_limit;
15721573

@@ -1823,6 +1824,10 @@ void branch_and_bound_t<i_t, f_t>::plunge_with(bfs_worker_t<i_t, f_t>* worker,
18231824
abs_gap = compute_user_abs_gap(original_lp_, upper_bound, lower_bound);
18241825
}
18251826

1827+
if (solver_status_ == mip_status_t::TIME_LIMIT || solver_status_ == mip_status_t::OPTIMAL) {
1828+
node_concurrent_halt_ = 1;
1829+
}
1830+
18261831
// If the solver exits early without consuming the local stack, or converged according to
18271832
// the gap rules while nodes are still pending, put those nodes back into the global queue
18281833
// before returning.
@@ -1840,6 +1845,9 @@ void branch_and_bound_t<i_t, f_t>::plunge_with(bfs_worker_t<i_t, f_t>* worker,
18401845
template <typename i_t, typename f_t>
18411846
void branch_and_bound_t<i_t, f_t>::launch_bfs_worker(bfs_worker_t<i_t, f_t>* worker)
18421847
{
1848+
// The status may change after the caller checks its search-loop condition.
1849+
if (solver_status_ != mip_status_t::UNSET) { return; }
1850+
18431851
bfs_worker_t<i_t, f_t>* idle_worker = bfs_worker_pool_.pop_idle_worker();
18441852
if (!idle_worker) return;
18451853

@@ -1976,8 +1984,7 @@ void branch_and_bound_t<i_t, f_t>::best_first_search_with(bfs_worker_t<i_t, f_t>
19761984
rel_gap = user_relative_gap(user_obj, user_lower);
19771985

19781986
if (abs_gap <= settings_.absolute_mip_gap_tol || rel_gap <= settings_.relative_mip_gap_tol) {
1979-
node_concurrent_halt_ = 1;
1980-
solver_status_ = mip_status_t::OPTIMAL;
1987+
solver_status_ = mip_status_t::OPTIMAL;
19811988
break;
19821989
}
19831990

@@ -1987,6 +1994,10 @@ void branch_and_bound_t<i_t, f_t>::best_first_search_with(bfs_worker_t<i_t, f_t>
19871994
}
19881995
}
19891996

1997+
if (solver_status_ == mip_status_t::TIME_LIMIT || solver_status_ == mip_status_t::OPTIMAL) {
1998+
node_concurrent_halt_ = 1;
1999+
}
2000+
19902001
// If the worker has still nodes in the queue (this can happen if it was stopped due to
19912002
// time limit, small gap or other reason), then do not add back to the pool to avoid
19922003
// constantly trying to start it again
@@ -2044,7 +2055,8 @@ void branch_and_bound_t<i_t, f_t>::dive_with(diving_worker_t<i_t, f_t>* worker,
20442055
}
20452056

20462057
if (toc(exploration_stats_.start_time) > settings_.time_limit) {
2047-
solver_status_ = mip_status_t::TIME_LIMIT;
2058+
node_concurrent_halt_ = 1;
2059+
solver_status_ = mip_status_t::TIME_LIMIT;
20482060
break;
20492061
}
20502062
if (dive_stats.nodes_explored >= diving_node_limit) { break; }
@@ -2063,7 +2075,8 @@ void branch_and_bound_t<i_t, f_t>::dive_with(diving_worker_t<i_t, f_t>* worker,
20632075
++dive_stats.nodes_explored;
20642076

20652077
if (lp_status == dual_status_t::TIME_LIMIT) {
2066-
solver_status_ = mip_status_t::TIME_LIMIT;
2078+
node_concurrent_halt_ = 1;
2079+
solver_status_ = mip_status_t::TIME_LIMIT;
20672080
break;
20682081
}
20692082
if (lp_status == dual_status_t::CONCURRENT_LIMIT) { break; }

cpp/src/cuts/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
set(CUTS_SRC_FILES
77
${CMAKE_CURRENT_SOURCE_DIR}/cuts.cpp
88
${CMAKE_CURRENT_SOURCE_DIR}/objective_step.cpp
9+
${CMAKE_CURRENT_SOURCE_DIR}/zero_half_mod2.cpp
910
)
1011

1112
set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES}

0 commit comments

Comments
 (0)