3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-07-02 13:26:10 +00:00

lp: gate Gomory-with-dio on genuine dio failures; separate config from runtime state (#9958)

## Summary

Improves the Diophantine (`dio`) integer-feasibility controller in
`int_solver`, and fixes a latent bug where the user's Gomory-cut
configuration could be silently overridden at runtime. Also includes the
earlier `lia_w` work: randomized hammer gates, the `int_hammer_period` /
`random_hammers` parameters, and the linear `dio_calls_period` recovery.

## Motivation

The controller used a **single field** both as the static
`lp.dio_cuts_enable_gomory` parameter and as the live "is Gomory
running" flag. It started running Gomory (and the gcd test) once
`dio_calls_period` crossed a hard-coded `16`. Because `dio_calls_period`
is also driven by the randomized hammer gate, on instances where `dio`
is only intermittently productive the period could be ratcheted past 16
*by chance*, turning on Gomory + gcd and thrashing — e.g. `dillig/20-14`
went from a 100s solve (deterministic) to a 600s timeout (randomized)
purely from this spurious activation.

## Changes

- **Separate config from runtime state.** Split the shared field into
`m_dio_cuts_enable_gomory` (static config, never mutated) and
`m_run_gomory_with_dio` (runtime flag). Toggling the runtime state can
no longer clobber the user's `dio_cuts_enable_gomory` parameter.
- **Trigger on genuine dio failures, not the period proxy.** Running
Gomory-with-dio now starts after a count of **consecutive `undef` dio
returns** (reset on a dio conflict) rather than when the
randomization-inflated period crosses a threshold — robust to
`random_hammers` gate variance.
- **Parameterize the threshold.** New `lp.dio_gomory_enable_period`
(default 16). Set it very large to never auto-start Gomory, so Gomory
follows `dio_cuts_enable_gomory` only.
- **Try `dio` before Gomory** in `check()` so a productive dio conflict
preempts Gomory on dio-dominated instances.

## Evaluation (QF_LIA, full set, 600s, seed 555 paired)

- Dio-before-Gomory: **+33** problems across the 6 `random_hammers x
int_hammer_period` cells (5/6 cells improve).
- New trigger (`dio_gomory_enable_period=32`, random): **6417** vs the
period-16 baseline **6409**; no short-cutoff regression.
- Linear `dio_calls_period` recovery: keeping it on is worth ~+20 vs
off; `decrease=1` slightly ahead of the default 2.

Default behavior (`dio_gomory_enable_period=16`) is byte-for-byte
equivalent to the previous threshold logic.

## Notes

Debug-only tracing used during analysis (the `dio_calls_period_trace`
parameter plus per-hammer / period-evolution verbose output) is **not**
included.

---------

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Lev Nachmanson 2026-06-25 14:21:44 -07:00 committed by GitHub
parent 6fd303c4b9
commit 57fb719007
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 72 additions and 15 deletions

View file

@ -45,6 +45,9 @@ namespace lp {
int_gcd_test m_gcd; int_gcd_test m_gcd;
unsigned m_initial_dio_calls_period; unsigned m_initial_dio_calls_period;
unsigned m_lcube_period; unsigned m_lcube_period;
// The number of consecutive genuine dio calls that returned undef, reset on a dio
// conflict. Drives the decision to start running Gomory with dio.
unsigned m_dio_undef_in_a_row = 0;
bool column_is_int_inf(unsigned j) const { bool column_is_int_inf(unsigned j) const {
return lra.column_is_int(j) && (!lia.value_is_int(j)); return lra.column_is_int(j) && (!lia.value_is_int(j));
@ -178,14 +181,16 @@ namespace lp {
if (r == lia_move::conflict) { if (r == lia_move::conflict) {
m_dio.explain(*this->m_ex); m_dio.explain(*this->m_ex);
lia.settings().dio_calls_period() = m_initial_dio_calls_period; lia.settings().dio_calls_period() = m_initial_dio_calls_period;
lia.settings().dio_enable_gomory_cuts() = false; m_dio_undef_in_a_row = 0;
lia.settings().stop_running_gomory_with_dio(); // dio was productive: stop running Gomory
lia.settings().set_run_gcd_test(false); lia.settings().set_run_gcd_test(false);
return lia_move::conflict; return lia_move::conflict;
} }
if (r == lia_move::undef) { if (r == lia_move::undef) {
lia.settings().dio_calls_period() *= 2; lia.settings().dio_calls_period() *= 2; // throttle dio scheduling on failure
if (lra.settings().dio_calls_period() >= 16) { ++m_dio_undef_in_a_row;
lia.settings().dio_enable_gomory_cuts() = true; if (m_dio_undef_in_a_row >= lra.settings().dio_gomory_enable_period()) {
lia.settings().start_running_gomory_with_dio(); // dio persistently unproductive: start running Gomory
lia.settings().set_run_gcd_test(true); lia.settings().set_run_gcd_test(true);
} }
} }
@ -194,8 +199,23 @@ namespace lp {
lp_settings& settings() { return lra.settings(); } lp_settings& settings() { return lra.settings(); }
// Decide whether a periodic heuristic fires on this call. When
// random_hammers is enabled the gate is drawn at random with the same
// 1/period expected rate instead of a deterministic "every k-th call"
// modulus: a deterministic period can phase-lock with the search on
// some families and drown the solver in conflicts while another handler
// is starved; randomizing the gate breaks that resonance.
bool hit_period(unsigned period) {
if (period <= 1)
return true;
if (settings().random_hammers())
return settings().random_next(period) == 0;
return m_number_of_calls % period == 0;
}
bool should_find_cube() { bool should_find_cube() {
return m_number_of_calls % settings().m_int_find_cube_period == 0; return hit_period(settings().m_int_find_cube_period);
} }
// The largest cube test is throttled exponentially: when the polyhedron // The largest cube test is throttled exponentially: when the polyhedron
@ -203,7 +223,7 @@ namespace lp {
// later, after more constraints are added, so each failure doubles the // later, after more constraints are added, so each failure doubles the
// period and a success resets it. // period and a success resets it.
bool should_find_lcube() { bool should_find_lcube() {
return settings().lcube() && m_number_of_calls % m_lcube_period == 0; return settings().lcube() && hit_period(m_lcube_period);
} }
lia_move find_lcube() { lia_move find_lcube() {
@ -220,18 +240,24 @@ namespace lp {
bool should_gomory_cut() { bool should_gomory_cut() {
bool dio_allows_gomory = !settings().dio() || settings().dio_enable_gomory_cuts() || bool dio_allows_gomory = !settings().dio() || settings().dio_enable_gomory_cuts() ||
m_dio.some_terms_are_ignored(); m_dio.some_terms_are_ignored();
return dio_allows_gomory && m_number_of_calls % settings().m_int_gomory_cut_period == 0; return dio_allows_gomory && hit_period(settings().m_int_gomory_cut_period);
} }
bool should_solve_dioph_eq() { bool should_solve_dioph_eq() {
return lia.settings().dio() && (m_number_of_calls % settings().dio_calls_period() == 0); bool ret = lia.settings().dio() && hit_period(settings().dio_calls_period());
if (!ret && lia.settings().dio_calls_period() > m_initial_dio_calls_period) {
unsigned dec = settings().dio_calls_period_decrease();
unsigned& period = lia.settings().dio_calls_period();
period = period > m_initial_dio_calls_period + dec ? period - dec : m_initial_dio_calls_period;
}
return ret;
} }
// HNF // HNF
bool should_hnf_cut() { bool should_hnf_cut() {
return (!settings().dio() || settings().dio_enable_hnf_cuts()) return (!settings().dio() || settings().dio_enable_hnf_cuts())
&& settings().enable_hnf() && m_number_of_calls % settings().hnf_cut_period() == 0; && settings().enable_hnf() && hit_period(settings().hnf_cut_period());
} }
lia_move hnf_cut() { lia_move hnf_cut() {
@ -266,12 +292,12 @@ namespace lp {
++m_number_of_calls; ++m_number_of_calls;
if (r == lia_move::undef) r = patch_basic_columns(); if (r == lia_move::undef) r = patch_basic_columns();
if (r == lia_move::undef && should_find_cube()) r = int_cube(lia)(); if (r == lia_move::undef && should_find_cube()) r = int_cube(lia)();
if (r == lia_move::undef && should_find_lcube()) r = find_lcube(); if (r == lia_move::undef && should_find_lcube()) r = find_lcube();
if (r == lia_move::undef) lra.move_non_basic_columns_to_bounds(); if (r == lia_move::undef) lra.move_non_basic_columns_to_bounds();
if (r == lia_move::undef && should_hnf_cut()) r = hnf_cut(); if (r == lia_move::undef && should_hnf_cut()) r = hnf_cut();
if (r == lia_move::undef && should_gomory_cut()) r = gomory(lia).get_gomory_cuts(2);
if (r == lia_move::undef && should_solve_dioph_eq()) r = solve_dioph_eq(); if (r == lia_move::undef && should_solve_dioph_eq()) r = solve_dioph_eq();
if (r == lia_move::undef && should_gomory_cut()) r = gomory(lia).get_gomory_cuts(2);
if (r == lia_move::undef) r = int_branch(lia)(); if (r == lia_move::undef) r = int_branch(lia)();
if (settings().get_cancel_flag()) r = lia_move::undef; if (settings().get_cancel_flag()) r = lia_move::undef;
return r; return r;

View file

@ -5,11 +5,15 @@ def_module_params(module_name='lp',
params=(('dio', BOOL, True, 'use Diophantine equalities'), params=(('dio', BOOL, True, 'use Diophantine equalities'),
('dio_branching_period', UINT, 100, 'Period of calling branching on undef in Diophantine handler'), ('dio_branching_period', UINT, 100, 'Period of calling branching on undef in Diophantine handler'),
('dio_cuts_enable_gomory', BOOL, False, 'enable Gomory cuts together with Diophantine cuts, only relevant when dioph_eq is true'), ('dio_cuts_enable_gomory', BOOL, False, 'enable Gomory cuts together with Diophantine cuts, only relevant when dioph_eq is true'),
('dio_gomory_enable_period', UINT, 16, 'number of consecutive unproductive (undef) Diophantine-handler calls after which the controller starts running Gomory cuts and the gcd test alongside dio; a dio conflict resets the count and stops them; set very large to never start them this way so Gomory follows dio_cuts_enable_gomory only'),
('dio_cuts_enable_hnf', BOOL, True, 'enable hnf cuts together with Diophantine cuts, only relevant when dioph_eq is true'), ('dio_cuts_enable_hnf', BOOL, True, 'enable hnf cuts together with Diophantine cuts, only relevant when dioph_eq is true'),
('dio_ignore_big_nums', BOOL, True, 'Ignore the terms with big numbers in the Diophantine handler, only relevant when dioph_eq is true'), ('dio_ignore_big_nums', BOOL, True, 'Ignore the terms with big numbers in the Diophantine handler, only relevant when dioph_eq is true'),
('dio_calls_period', UINT, 1, 'Period of calling the Diophantine handler in the final_check()'), ('dio_calls_period', UINT, 1, 'Period of calling the Diophantine handler in the final_check()'),
('dio_calls_period_decrease', UINT, 2, 'Amount by which dio_calls_period is decreased on each final_check() call where the Diophantine handler is not triggered, until it returns to its initial value'),
('dio_run_gcd', BOOL, False, 'Run the GCD heuristic if dio is on, if dio is disabled the option is not used'), ('dio_run_gcd', BOOL, False, 'Run the GCD heuristic if dio is on, if dio is disabled the option is not used'),
('lcube', BOOL, True, 'use the largest cube test for integer feasibility'), ('lcube', BOOL, True, 'use the largest cube test for integer feasibility'),
('lcube_flips', UINT, 16, 'maximal number of coordinate flips when repairing the rounded largest cube center, only relevant when lcube is true'), ('lcube_flips', UINT, 16, 'maximal number of coordinate flips when repairing the rounded largest cube center, only relevant when lcube is true'),
('int_hammer_period', UINT, 4, 'period (in final_check calls) for the integer cut/cube heuristics (find_cube, hnf, gomory); a smaller value calls them more often'),
('random_hammers', BOOL, True, 'draw the periodic integer heuristic gates (find_cube, lcube, hnf, gomory, dio) at random with the same 1/period rate instead of a deterministic every-k-th-call modulus'),
)) ))

View file

@ -37,13 +37,21 @@ void lp::lp_settings::updt_params(params_ref const& _p) {
auto eps = p.arith_epsilon(); auto eps = p.arith_epsilon();
m_epsilon = rational(std::max(1, (int)(100000*eps)), 100000); m_epsilon = rational(std::max(1, (int)(100000*eps)), 100000);
m_dio = lp_p.dio(); m_dio = lp_p.dio();
m_dio_enable_gomory_cuts = lp_p.dio_cuts_enable_gomory(); m_dio_cuts_enable_gomory = lp_p.dio_cuts_enable_gomory();
m_dio_gomory_enable_period = lp_p.dio_gomory_enable_period();
m_dio_enable_hnf_cuts = lp_p.dio_cuts_enable_hnf(); m_dio_enable_hnf_cuts = lp_p.dio_cuts_enable_hnf();
m_dump_bound_lemmas = p.arith_dump_bound_lemmas(); m_dump_bound_lemmas = p.arith_dump_bound_lemmas();
m_dio_ignore_big_nums = lp_p.dio_ignore_big_nums(); m_dio_ignore_big_nums = lp_p.dio_ignore_big_nums();
m_dio_calls_period = lp_p.dio_calls_period(); m_dio_calls_period = lp_p.dio_calls_period();
m_dio_calls_period_decrease = lp_p.dio_calls_period_decrease();
m_dio_run_gcd = lp_p.dio_run_gcd(); m_dio_run_gcd = lp_p.dio_run_gcd();
m_random_hammers = lp_p.random_hammers();
m_lcube = lp_p.lcube(); m_lcube = lp_p.lcube();
m_lcube_flips = lp_p.lcube_flips(); m_lcube_flips = lp_p.lcube_flips();
unsigned hammer_period = lp_p.int_hammer_period();
SASSERT(hammer_period != 0);
m_int_find_cube_period = hammer_period;
m_int_gomory_cut_period = hammer_period;
m_hnf_cut_period = hammer_period;
m_max_conflicts = p.max_conflicts(); m_max_conflicts = p.max_conflicts();
} }

View file

@ -258,12 +258,16 @@ private:
bool m_print_external_var_name = false; bool m_print_external_var_name = false;
bool m_propagate_eqs = false; bool m_propagate_eqs = false;
bool m_dio = false; bool m_dio = false;
bool m_dio_enable_gomory_cuts = false; bool m_dio_cuts_enable_gomory = false;
bool m_run_gomory_with_dio = false;
unsigned m_dio_gomory_enable_period = 16;
bool m_dio_enable_hnf_cuts = true; bool m_dio_enable_hnf_cuts = true;
bool m_dump_bound_lemmas = false; bool m_dump_bound_lemmas = false;
bool m_dio_ignore_big_nums = false; bool m_dio_ignore_big_nums = false;
unsigned m_dio_calls_period = 4; unsigned m_dio_calls_period = 4;
unsigned m_dio_calls_period_decrease = 2;
bool m_dio_run_gcd = true; bool m_dio_run_gcd = true;
bool m_random_hammers = true;
bool m_lcube = true; bool m_lcube = true;
unsigned m_lcube_flips = 16; unsigned m_lcube_flips = 16;
public: public:
@ -271,6 +275,10 @@ public:
unsigned lcube_flips() const { return m_lcube_flips; } unsigned lcube_flips() const { return m_lcube_flips; }
unsigned dio_calls_period() const { return m_dio_calls_period; } unsigned dio_calls_period() const { return m_dio_calls_period; }
unsigned & dio_calls_period() { return m_dio_calls_period; } unsigned & dio_calls_period() { return m_dio_calls_period; }
unsigned dio_calls_period_decrease() const { return m_dio_calls_period_decrease; }
unsigned & dio_calls_period_decrease() { return m_dio_calls_period_decrease; }
bool random_hammers() const { return m_random_hammers; }
bool & random_hammers() { return m_random_hammers; }
bool print_external_var_name() const { return m_print_external_var_name; } bool print_external_var_name() const { return m_print_external_var_name; }
bool propagate_eqs() const { return m_propagate_eqs;} bool propagate_eqs() const { return m_propagate_eqs;}
unsigned hnf_cut_period() const { return m_hnf_cut_period; } unsigned hnf_cut_period() const { return m_hnf_cut_period; }
@ -278,8 +286,19 @@ public:
unsigned random_next() { return m_rand(); } unsigned random_next() { return m_rand(); }
unsigned random_next(unsigned u ) { return m_rand(u); } unsigned random_next(unsigned u ) { return m_rand(u); }
bool dio() { return m_dio; } bool dio() { return m_dio; }
bool & dio_enable_gomory_cuts() { return m_dio_enable_gomory_cuts; } // Static config: did the user request Gomory cuts up front? (lp.dio_cuts_enable_gomory)
bool dio_enable_gomory_cuts() const { return m_dio && m_dio_enable_gomory_cuts; } bool dio_cuts_enable_gomory() const { return m_dio_cuts_enable_gomory; }
// dio_calls_period at which the Diophantine back-off starts running Gomory (lp.dio_gomory_enable_period)
unsigned dio_gomory_enable_period() const { return m_dio_gomory_enable_period; }
// Runtime flag owned by the Diophantine controller, kept separate from the static
// config above so toggling it never clobbers the user's parameter: once dio has
// backed off enough it starts running Gomory cuts alongside dio, and a productive
// dio conflict stops them again.
void start_running_gomory_with_dio() { m_run_gomory_with_dio = true; }
void stop_running_gomory_with_dio() { m_run_gomory_with_dio = false; }
// Effective state read by should_gomory_cut(): allowed if either the user enabled it
// statically or the dio controller started running it, guarded by dio being active.
bool dio_enable_gomory_cuts() const { return m_dio && (m_dio_cuts_enable_gomory || m_run_gomory_with_dio); }
bool dio_run_gcd() const { return m_dio && m_dio_run_gcd; } bool dio_run_gcd() const { return m_dio && m_dio_run_gcd; }
bool dio_enable_hnf_cuts() const { return m_dio && m_dio_enable_hnf_cuts; } bool dio_enable_hnf_cuts() const { return m_dio && m_dio_enable_hnf_cuts; }
bool dio_ignore_big_nums() const { return m_dio_ignore_big_nums; } bool dio_ignore_big_nums() const { return m_dio_ignore_big_nums; }