3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-08-09 23:42:21 +00:00

Fix linprobe perf regression and refactor the probe into a reusable combinator

Perf: remove combined_solver::try_linprobe

It ran the *non-incremental* solver1 only when m_inc_mode or assumptions were
present, i.e. exactly the modes where combined_solver mandates solver2, so
solver1 re-preprocessed the whole assertion stack on every check-sat. Its
wall-clock scoped_timer did not bound that work (linprobe_timeout=1 was slower
than 100), it consumed the caller's rlimit and so changed verdicts, and it made
rlimit-based runs non-deterministic: the same binary on
queries-FStar.UInt128.smt2 returned different unsat counts depending on whether
stdout was redirected or piped.

On the F* ulib queries this is a 3.99x aggregate speedup (224.35s -> 56.29s over
10 files; UInt128 80.2s -> 20.7s, BV 19.9s -> 1.7s) with verdicts identical to
master. The feature itself is unaffected: it lives in the smt tactic, which is
what arith.nl.linprobe documents, and solver1 reaches it via mk_smt_tactic.

Params: declare arith.nl.linprobe_mode and arith.nl.linprobe_timeout

arith.nl.linprobe_mode was read by raw string lookup in four places but never
declared, so it was invisible to -pm and rejected by set-option. The 100ms
timeout was hard-coded twice, in two different libraries.

Refactor

- Move the generic part of linprobe_tactic to tactical.{h,cpp} beside or_else as
  or_else_no_user_propagate(); the class was an or_else reimplementation whose
  only new behaviour was bypassing t1 once user propagators are registered.
  smt_tactic.cpp shrinks from 231 to 100 lines.
- unary_tactical: forward the ten missing user_propagate_* methods so wrappers
  such as using_params do not drop propagator support.
- nla_core: drop the cached m_linprobe flag and use params().arith_nl_linprobe_mode()
  through a new core::linprobe_mode(), matching how every other nla parameter is read.
- theory_lra: replace a per-final-check string parameter lookup with
  m_nla->linprobe_mode().
- mk_smt_tactic_using: restore mk_smt_tactic_core_using as the fallback so
  parallel.enable keeps selecting mk_parallel_smt_tactic.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Lev Nachmanson 2026-08-07 09:02:36 -07:00
parent 98bb6245ca
commit 36738a0394
10 changed files with 180 additions and 203 deletions

View file

@ -22,7 +22,6 @@ namespace nla {
if (add_lemma())
break;
}
}
bool monomial_bounds::is_too_big(mpq const &q) const {
@ -1131,3 +1130,4 @@ namespace nla {
}
}

View file

@ -27,7 +27,6 @@ core::core(lp::lar_solver& s, params_ref const& p, reslimit & lim) :
lra(s),
m_reslim(lim),
m_params(p),
m_linprobe(p.get_bool("arith.nl.linprobe_mode", false)),
m_tangents(this),
m_basics(this),
m_order(this),
@ -55,7 +54,8 @@ core::core(lp::lar_solver& s, params_ref const& p, reslimit & lim) :
}
void core::updt_params(params_ref const& p) {
m_linprobe = p.get_bool("arith.nl.linprobe_mode", false);
// m_params is a view on the params_ref owned by the caller, so nla parameters
// (including arith.nl.linprobe_mode) are always read back through params().
m_grobner.updt_params(p);
}
@ -1547,7 +1547,7 @@ bool core::propagate() {
propagated = true;
if (m_monomial_bounds.tighten_lp_bounds())
propagated = true;
if (m_linprobe) {
if (linprobe_mode()) {
if (m_monomial_bounds.propagate_linear_bounds())
propagated = true;
}

View file

@ -73,7 +73,6 @@ class core {
lp::lar_solver& lra;
reslimit& m_reslim;
smt_params_helper m_params;
bool m_linprobe;
std::function<bool(lpvar)> m_relevant;
vector<lemma> m_lemmas;
vector<ineq> m_literals;
@ -187,6 +186,9 @@ public:
smt_params_helper const & params() const { return m_params; }
// true when nla is restricted to the monomial linearization used by the linprobe pass
bool linprobe_mode() const { return params().arith_nl_linprobe_mode(); }
// returns true if the combination of the Horner's schema and Grobner Basis should be called
bool need_run_horner() const {
return params().arith_nl_horner() && lp_settings().stats().m_nla_calls % params().arith_nl_horner_frequency() == 0;

View file

@ -56,6 +56,7 @@ namespace nla {
vector<lp::fixed_equality> const& fixed_equalities() const;
vector<lp::equality> const& equalities() const;
bool should_check_feasible() const { return m_core->should_check_feasible(); }
bool linprobe_mode() const { return m_core->linprobe_mode(); }
const vector<nla::lemma>& lemmas() const;

View file

@ -100,6 +100,8 @@ def_module_params(module_name='smt',
('arith.nl.reduce_pseudo_linear', BOOL, True, 'create incremental linearization axioms for pseudo-linear monomials'),
('arith.nl.delay', UINT, 10, 'number of calls to final check before invoking bounded nlsat check'),
('arith.nl.linprobe', BOOL, True, 'run a short monomial linearization probe before the SMT tactic'),
('arith.nl.linprobe_mode', BOOL, False, 'restrict nonlinear propagation to the monomial linearization used by the linprobe pass'),
('arith.nl.linprobe_timeout', UINT, 100, 'timeout in milliseconds given to the monomial linearization probe'),
('arith.nl.propagate_linear_monomials', BOOL, True, 'propagate linear monomials'),
('arith.nl.optimize_bounds', BOOL, True, 'enable bounds optimization'),
('arith.nl.propagate_fixed_rows', BOOL, False, 'scan LP rows for fixed variables'),

View file

@ -1750,7 +1750,7 @@ public:
is_sat = make_feasible();
if (ctx().inconsistent())
return FC_CONTINUE;
if (nla_progress && is_sat == l_true && ctx().get_params().get_bool("arith.nl.linprobe_mode", false))
if (nla_progress && is_sat == l_true && m_nla && m_nla->linprobe_mode())
return FC_CONTINUE;
final_check_status st = FC_DONE;
bool int_undef = false;

View file

@ -21,14 +21,11 @@ Notes:
#include "util/scoped_timer.h"
#include "util/common_msgs.h"
#include "ast/ast_pp.h"
#include "params/smt_params_helper.hpp"
#include "solver/solver.h"
#include "solver/combined_solver_params.hpp"
#include <atomic>
#define PS_VB_LVL 15
static constexpr unsigned linprobe_timeout_ms = 100;
/**
\brief Implementation of the solver API that combines two given solvers.
@ -59,7 +56,6 @@ private:
bool m_inc_mode;
bool m_check_sat_executed;
bool m_use_solver1_results;
bool m_has_callbacks;
ref<solver> m_solver1;
ref<solver> m_solver2;
// We delay sending assertions to solver 2
@ -118,28 +114,6 @@ private:
}
}
lbool try_linprobe(unsigned num_assumptions, expr* const* assumptions) {
if (m_has_callbacks || !smt_params_helper(get_params()).arith_nl_linprobe())
return l_undef;
IF_VERBOSE(PS_VB_LVL, verbose_stream() << "(combined-solver \"using linprobe\")\n";);
m_use_solver1_results = true;
aux_timeout_eh eh(m_solver1.get());
lbool r = l_undef;
try {
scoped_timer timer(linprobe_timeout_ms, &eh);
r = m_solver1->check_sat_core(num_assumptions, assumptions);
}
catch (z3_exception&) {
if (!eh.m_canceled)
throw;
}
if (r != l_undef && !eh.m_canceled)
return r;
m_use_solver1_results = false;
return l_undef;
}
public:
combined_solver(solver * s1, solver * s2, params_ref const & p):
solver(s1->get_manager()) {
@ -149,7 +123,6 @@ public:
m_inc_mode = false;
m_check_sat_executed = false;
m_use_solver1_results = true;
m_has_callbacks = false;
}
solver* translate(ast_manager& m, params_ref const& p) override {
@ -160,7 +133,6 @@ public:
r->m_inc_mode = m_inc_mode;
r->m_check_sat_executed = m_check_sat_executed;
r->m_use_solver1_results = m_use_solver1_results;
r->m_has_callbacks = m_has_callbacks;
return r;
}
@ -240,12 +212,6 @@ public:
m_check_sat_executed = true;
m_use_solver1_results = false;
if (m_inc_mode || get_num_assumptions() != 0 || num_assumptions > 0 || m_ignore_solver1) {
lbool const r = try_linprobe(num_assumptions, assumptions);
if (r != l_undef)
return r;
}
if (get_num_assumptions() != 0 ||
num_assumptions > 0 || // assumptions were provided
m_ignore_solver1) {
@ -386,7 +352,6 @@ public:
void register_on_clause(void* ctx, user_propagator::on_clause_eh_t& on_clause) override {
switch_inc_mode();
m_has_callbacks = true;
m_solver2->register_on_clause(ctx, on_clause);
}
@ -396,57 +361,46 @@ public:
user_propagator::pop_eh_t& pop_eh,
user_propagator::fresh_eh_t& fresh_eh) override {
switch_inc_mode();
m_has_callbacks = true;
m_solver2->user_propagate_init(ctx, push_eh, pop_eh, fresh_eh);
}
void user_propagate_register_fixed(user_propagator::fixed_eh_t& fixed_eh) override {
m_has_callbacks = true;
m_solver2->user_propagate_register_fixed(fixed_eh);
}
void user_propagate_register_final(user_propagator::final_eh_t& final_eh) override {
m_has_callbacks = true;
m_solver2->user_propagate_register_final(final_eh);
}
void user_propagate_register_eq(user_propagator::eq_eh_t& eq_eh) override {
m_has_callbacks = true;
m_solver2->user_propagate_register_eq(eq_eh);
}
void user_propagate_register_diseq(user_propagator::eq_eh_t& diseq_eh) override {
m_has_callbacks = true;
m_solver2->user_propagate_register_diseq(diseq_eh);
}
void user_propagate_register_on_binding(user_propagator::binding_eh_t& binding_eh) override {
m_has_callbacks = true;
m_solver2->user_propagate_register_on_binding(binding_eh);
}
void user_propagate_register_expr(expr* e) override {
m_has_callbacks = true;
m_solver2->user_propagate_register_expr(e);
}
void user_propagate_register_created(user_propagator::created_eh_t& r) override {
m_has_callbacks = true;
m_solver2->user_propagate_register_created(r);
}
void user_propagate_register_decide(user_propagator::decide_eh_t& r) override {
m_has_callbacks = true;
m_solver2->user_propagate_register_decide(r);
}
void user_propagate_clear() override {
m_has_callbacks = false;
m_solver2->user_propagate_clear();
}
void user_propagate_initialize_value(expr* var, expr* value) override {
m_has_callbacks = true;
m_solver1->user_propagate_initialize_value(var, value);
m_solver2->user_propagate_initialize_value(var, value);
}

View file

@ -25,14 +25,18 @@ Author:
namespace {
constexpr unsigned linprobe_timeout_ms = 100;
/**
\brief Parameters for the monomial linearization probe.
Only cheap, deterministic nonlinear propagation is enabled, and the probe is
asked to fail (rather than report unknown) so that the full solver is used as
a fallback.
*/
params_ref probe_params(params_ref const& p) {
params_ref r = p;
r.set_bool("auto_config", false);
r.set_bool("parallel.enable", false);
r.set_uint("arith.solver", 6);
r.set_bool("arith.nl.linprobe", true);
r.set_bool("arith.nl.linprobe_mode", true);
r.set_bool("arith.nl.propagate_fixed_rows", true);
r.set_bool("arith.nl.propagate_linear_monomials", true);
@ -59,150 +63,6 @@ params_ref fallback_params(params_ref const& p) {
return r;
}
class linprobe_tactic : public tactic {
tactic_ref m_probe;
tactic_ref m_fallback;
params_ref m_params;
bool m_has_callbacks = false;
public:
linprobe_tactic(tactic* probe, tactic* fallback, params_ref const& p):
m_probe(probe),
m_fallback(fallback),
m_params(p) {
updt_params(p);
}
char const* name() const override { return "linprobe"; }
void operator()(goal_ref const& in, goal_ref_buffer& result) override {
if (!smt_params_helper(m_params).arith_nl_linprobe() || m_has_callbacks) {
(*m_fallback)(in, result);
return;
}
goal orig(*in);
try {
(*m_probe)(in, result);
return;
}
catch (tactic_exception&) {
result.reset();
in->reset_all();
in->copy_from(orig);
}
(*m_fallback)(in, result);
}
void updt_params(params_ref const& p) override {
m_params.copy(p);
m_probe->updt_params(probe_params(p));
m_fallback->updt_params(fallback_params(p));
}
void collect_param_descrs(param_descrs& r) override {
m_fallback->collect_param_descrs(r);
}
void collect_statistics(statistics& st) const override {
m_probe->collect_statistics(st);
m_fallback->collect_statistics(st);
}
void reset_statistics() override {
m_probe->reset_statistics();
m_fallback->reset_statistics();
}
void cleanup() override {
m_probe->cleanup();
m_fallback->cleanup();
}
void reset() override {
m_probe->reset();
m_fallback->reset();
}
void set_logic(symbol const& l) override {
m_probe->set_logic(l);
m_fallback->set_logic(l);
}
void set_progress_callback(progress_callback* callback) override {
m_probe->set_progress_callback(callback);
m_fallback->set_progress_callback(callback);
}
tactic* translate(ast_manager& m) override {
return alloc(linprobe_tactic, m_probe->translate(m), m_fallback->translate(m), m_params);
}
void register_on_clause(void* ctx, user_propagator::on_clause_eh_t& on_clause) override {
m_has_callbacks = true;
m_fallback->register_on_clause(ctx, on_clause);
}
void user_propagate_init(
void* ctx,
user_propagator::push_eh_t& push_eh,
user_propagator::pop_eh_t& pop_eh,
user_propagator::fresh_eh_t& fresh_eh) override {
m_has_callbacks = true;
m_fallback->user_propagate_init(ctx, push_eh, pop_eh, fresh_eh);
}
void user_propagate_register_fixed(user_propagator::fixed_eh_t& fixed_eh) override {
m_has_callbacks = true;
m_fallback->user_propagate_register_fixed(fixed_eh);
}
void user_propagate_register_final(user_propagator::final_eh_t& final_eh) override {
m_has_callbacks = true;
m_fallback->user_propagate_register_final(final_eh);
}
void user_propagate_register_eq(user_propagator::eq_eh_t& eq_eh) override {
m_has_callbacks = true;
m_fallback->user_propagate_register_eq(eq_eh);
}
void user_propagate_register_diseq(user_propagator::eq_eh_t& diseq_eh) override {
m_has_callbacks = true;
m_fallback->user_propagate_register_diseq(diseq_eh);
}
void user_propagate_register_on_binding(user_propagator::binding_eh_t& binding_eh) override {
m_has_callbacks = true;
m_fallback->user_propagate_register_on_binding(binding_eh);
}
void user_propagate_register_expr(expr* e) override {
m_has_callbacks = true;
m_fallback->user_propagate_register_expr(e);
}
void user_propagate_register_created(user_propagator::created_eh_t& created_eh) override {
m_has_callbacks = true;
m_fallback->user_propagate_register_created(created_eh);
}
void user_propagate_register_decide(user_propagator::decide_eh_t& decide_eh) override {
m_has_callbacks = true;
m_fallback->user_propagate_register_decide(decide_eh);
}
void user_propagate_clear() override {
m_has_callbacks = false;
m_fallback->user_propagate_clear();
}
void user_propagate_initialize_value(expr* var, expr* value) override {
m_has_callbacks = true;
m_fallback->user_propagate_initialize_value(var, value);
}
};
tactic* mk_raw_smt_tactic(ast_manager& m, params_ref const& p) {
sat_params sp(p);
if (sp.smt())
@ -212,20 +72,33 @@ tactic* mk_raw_smt_tactic(ast_manager& m, params_ref const& p) {
return mk_smt_tactic_core(m, p);
}
tactic* mk_linprobe_tactic(ast_manager& m, params_ref const& p) {
/**
\brief Run a short monomial linearization probe before \c fallback.
\c fallback is only reached when the probe fails to close the goal, and it is
the only branch that supports user propagation.
*/
tactic* mk_linprobe_tactic(ast_manager& m, params_ref const& p, tactic* fallback) {
params_ref pp = probe_params(p);
tactic* probe = try_for(mk_smt_tactic_core(m, pp), linprobe_timeout_ms);
return alloc(linprobe_tactic, probe, mk_raw_smt_tactic(m, p), p);
unsigned timeout = smt_params_helper(pp).arith_nl_linprobe_timeout();
tactic* probe = using_params(try_for(mk_smt_tactic_core(m, pp), timeout), pp);
return or_else_no_user_propagate(probe, fallback, p, [](params_ref const& q) {
return smt_params_helper(q).arith_nl_linprobe();
});
}
}
tactic * mk_smt_tactic(ast_manager & m, params_ref const & p) {
return mk_linprobe_tactic(m, p);
params_ref fp = fallback_params(p);
return mk_linprobe_tactic(m, p, using_params(mk_raw_smt_tactic(m, fp), fp));
}
tactic * mk_smt_tactic_using(ast_manager& m, bool auto_config, params_ref const& p) {
params_ref q = p;
q.set_bool("auto_config", auto_config);
return using_params(mk_linprobe_tactic(m, q), q);
params_ref fp = fallback_params(q);
sat_params sp(fp);
tactic* fallback = sp.euf() ? mk_sat_tactic(m, fp) : mk_smt_tactic_core_using(m, auto_config, fp);
return mk_linprobe_tactic(m, q, using_params(fallback, fp));
}

View file

@ -448,6 +448,126 @@ tactic * or_else(tactic * t1, tactic * t2, tactic * t3, tactic * t4, tactic * t5
return or_else(10, ts);
}
class or_else_no_user_propagate_tactical : public binary_tactical {
params_ref m_params;
std::function<bool(params_ref const&)> m_use_t1;
bool m_has_callbacks = false;
bool use_t1() const {
return !m_has_callbacks && (!m_use_t1 || m_use_t1(m_params));
}
public:
or_else_no_user_propagate_tactical(tactic * t1, tactic * t2, params_ref const & p,
std::function<bool(params_ref const&)> const & use_t1):
binary_tactical(t1, t2),
m_params(p),
m_use_t1(use_t1) {
}
char const* name() const override { return "or_else_no_user_propagate"; }
void operator()(goal_ref const & in, goal_ref_buffer & result) override {
m_clean = false;
if (!use_t1()) {
m_t2->operator()(in, result);
return;
}
goal orig(*(in.get()));
try {
m_t1->operator()(in, result);
return;
}
catch (tactic_exception &) {
result.reset();
}
catch (rewriter_exception &) {
result.reset();
}
in->reset_all();
in->copy_from(orig);
m_t2->operator()(in, result);
}
void updt_params(params_ref const & p) override {
m_params.copy(p);
binary_tactical::updt_params(p);
}
tactic * translate(ast_manager & m) override {
return alloc(or_else_no_user_propagate_tactical, m_t1->translate(m), m_t2->translate(m), m_params, m_use_t1);
}
void register_on_clause(void * ctx, user_propagator::on_clause_eh_t & on_clause) override {
m_has_callbacks = true;
m_t2->register_on_clause(ctx, on_clause);
}
void user_propagate_init(
void * ctx,
user_propagator::push_eh_t & push_eh,
user_propagator::pop_eh_t & pop_eh,
user_propagator::fresh_eh_t & fresh_eh) override {
m_has_callbacks = true;
m_t2->user_propagate_init(ctx, push_eh, pop_eh, fresh_eh);
}
void user_propagate_register_fixed(user_propagator::fixed_eh_t & fixed_eh) override {
m_has_callbacks = true;
m_t2->user_propagate_register_fixed(fixed_eh);
}
void user_propagate_register_final(user_propagator::final_eh_t & final_eh) override {
m_has_callbacks = true;
m_t2->user_propagate_register_final(final_eh);
}
void user_propagate_register_eq(user_propagator::eq_eh_t & eq_eh) override {
m_has_callbacks = true;
m_t2->user_propagate_register_eq(eq_eh);
}
void user_propagate_register_diseq(user_propagator::eq_eh_t & diseq_eh) override {
m_has_callbacks = true;
m_t2->user_propagate_register_diseq(diseq_eh);
}
void user_propagate_register_on_binding(user_propagator::binding_eh_t & binding_eh) override {
m_has_callbacks = true;
m_t2->user_propagate_register_on_binding(binding_eh);
}
void user_propagate_register_expr(expr * e) override {
m_has_callbacks = true;
m_t2->user_propagate_register_expr(e);
}
void user_propagate_register_created(user_propagator::created_eh_t & created_eh) override {
m_has_callbacks = true;
m_t2->user_propagate_register_created(created_eh);
}
void user_propagate_register_decide(user_propagator::decide_eh_t & decide_eh) override {
m_has_callbacks = true;
m_t2->user_propagate_register_decide(decide_eh);
}
void user_propagate_clear() override {
m_has_callbacks = false;
m_t2->user_propagate_clear();
}
void user_propagate_initialize_value(expr * var, expr * value) override {
m_has_callbacks = true;
m_t2->user_propagate_initialize_value(var, value);
}
};
tactic * or_else_no_user_propagate(tactic * t1, tactic * t2, params_ref const & p,
std::function<bool(params_ref const&)> use_t1) {
return alloc(or_else_no_user_propagate_tactical, t1, t2, p, use_t1);
}
#ifdef SINGLE_THREAD
tactic* par(unsigned num, tactic* const* ts) {
@ -883,6 +1003,19 @@ public:
void reset() override { m_t->reset(); }
void set_logic(symbol const& l) override { m_t->set_logic(l); }
void set_progress_callback(progress_callback * callback) override { m_t->set_progress_callback(callback); }
void register_on_clause(void* ctx, user_propagator::on_clause_eh_t& on_clause) override { m_t->register_on_clause(ctx, on_clause); }
void user_propagate_init(
void* ctx,
user_propagator::push_eh_t& push_eh,
user_propagator::pop_eh_t& pop_eh,
user_propagator::fresh_eh_t& fresh_eh) override { m_t->user_propagate_init(ctx, push_eh, pop_eh, fresh_eh); }
void user_propagate_register_fixed(user_propagator::fixed_eh_t& fixed_eh) override { m_t->user_propagate_register_fixed(fixed_eh); }
void user_propagate_register_final(user_propagator::final_eh_t& final_eh) override { m_t->user_propagate_register_final(final_eh); }
void user_propagate_register_eq(user_propagator::eq_eh_t& eq_eh) override { m_t->user_propagate_register_eq(eq_eh); }
void user_propagate_register_diseq(user_propagator::eq_eh_t& diseq_eh) override { m_t->user_propagate_register_diseq(diseq_eh); }
void user_propagate_register_on_binding(user_propagator::binding_eh_t& binding_eh) override { m_t->user_propagate_register_on_binding(binding_eh); }
void user_propagate_register_created(user_propagator::created_eh_t& created_eh) override { m_t->user_propagate_register_created(created_eh); }
void user_propagate_register_decide(user_propagator::decide_eh_t& decide_eh) override { m_t->user_propagate_register_decide(decide_eh); }
void user_propagate_register_expr(expr* e) override { m_t->user_propagate_register_expr(e); }
void user_propagate_clear() override { m_t->user_propagate_clear(); }
void user_propagate_initialize_value(expr* var, expr* value) override { m_t->user_propagate_initialize_value(var, value); }

View file

@ -44,6 +44,18 @@ tactic * or_else(tactic * t1, tactic * t2, tactic * t3, tactic * t4, tactic * t5
tactic * or_else(tactic * t1, tactic * t2, tactic * t3, tactic * t4, tactic * t5, tactic * t6, tactic * t7, tactic * t8, tactic * t9);
tactic * or_else(tactic * t1, tactic * t2, tactic * t3, tactic * t4, tactic * t5, tactic * t6, tactic * t7, tactic * t8, tactic * t9, tactic * t10);
/**
\brief Variant of or_else(t1, t2) for the case where only \c t2 supports user
propagation. All user propagator and on-clause registrations are forwarded to
\c t2 only, and once any of them is registered \c t1 is bypassed entirely.
The optional \c use_t1 predicate is evaluated on the current parameters before
every invocation and can additionally disable \c t1. \c p seeds those
parameters, so the predicate is meaningful even when updt_params is never called.
*/
tactic * or_else_no_user_propagate(tactic * t1, tactic * t2, params_ref const & p,
std::function<bool(params_ref const &)> use_t1 = nullptr);
tactic * repeat(tactic * t, unsigned max = UINT_MAX);
/**
\brief Fails if \c t produces more than \c threshold subgoals.