3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-06-27 19:08:49 +00:00

Parallel tactic (#9824) (#9825)

Add new parallel algorithm as a tactic (parallel_tactical2.cpp)
Don't port over old experiments from smt_parallel that we aren't using
(sls, inprocessing, failed_literal_mode for bb detection)
Fix bugs: lease cancellation/reslimit race condition, involves changing
lease epoch to simple boolean flag
Also, now there is a single shared set of params for the tactic and
smt_parallel

**Test runs for the parallel_tactical2 vs old smt_parallel version:**
run-2747-Z3-threads-4-qflia-30s-stats.md
run-2746-Z3-threads-4-qflia-30s-parallel_tactic-stats.md
run-2745-Z3-threads-1-qfbv-30s-stats.md
run-3013-Z3-threads-4-qfbv-30s-parallel_tactic-stats.md --> note this is
indeed run-3013, I reran after a bugfix in inc_sat_solver
run-2743-Z3-threads-4-qfnia-30s-stats.md
run-2742-Z3-threads-4-qfnia-30s-parallel_tactic-stats.md

**Test runs for the new smt_parallel with bugfixes:**
run-2801-Z3-threads-4-qflia-30s-smtparallel-bugfixes-stats.md,
run-2800-Z3-threads-4-qflia-30s-smtparallel-bugfixes-stats.md
run-2797-Z3-threads-4-qfnia-30s-smtparallel-bugfixes-stats.md
compare to old smt_parallel:
run-2747-Z3-threads-4-qflia-30s-stats.md
run-2743-Z3-threads-4-qfnia-30s-stats.md

Note that there is a slight regression on lia in run-2800. The source of
this appears to be the new new LP largest-cube LIA heuristic param,
which is enabled by default. disabling this param in run-2801 restored
performance (I didn't change this in this PR though, just something to
note)

http://mtzguido.tplinkdns.com:8081/z3/compare_stats.html

---------

Signed-off-by: Nikolaj Bjorner <nbjorner@microsoft.com>
Co-authored-by: Ilana Shapiro <ilanashapiro@Ilanas-MacBook-Pro.local>
Co-authored-by: Ilana Shapiro <ilanashapiro@Ilanas-MBP.localdomain>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Nikolaj Bjorner 2026-06-26 09:36:15 -07:00 committed by GitHub
parent 15f33f458d
commit 612fab1c9a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 2694 additions and 1796 deletions

View file

@ -28,7 +28,6 @@ z3_add_component(params
seq_rewriter_params.pyg
sls_params.pyg
smt_params_helper.pyg
smt_parallel_params.pyg
solver_params.pyg
tactic_params.pyg
EXTRA_REGISTER_MODULE_HEADERS

View file

@ -1,12 +0,0 @@
def_module_params('smt_parallel',
export=True,
description='Experimental parameters for parallel solving',
params=(
('inprocessing', BOOL, False, 'integrate in-processing as a heuristic simplification'),
('sls', BOOL, False, 'add sls-tactic as a separate worker thread outside the search tree parallelism'),
('num_global_bb_fl_threads', UINT, 0, 'run failed-literal backbone worker threads; default is 0 (off), supported values are 1 (negative mode only) or 2 (negative and positive mode)'),
('num_global_bb_batch_threads', UINT, 0, 'run Janota-style chunking backbone worker threads; default is 0 (off), supported values are 1 (negative mode only) or 2 (negative and positive mode)'),
('local_backbones', BOOL, False, 'enable local backbones experiment within the search tree parallelism'),
('core_minimize', BOOL, True, 'minimize unsat cores used for parallel cube backtracking'),
('ablate_backtracking', BOOL, False, 'ablation: pass entire cube as core instead of unsat core during backtracking'),
))

View file

@ -132,6 +132,8 @@ namespace sat {
m_best_phase.reset();
m_phase.reset();
m_prev_phase.reset();
m_phase_birthdate.reset();
m_best_phase_birthdate.reset();
m_assigned_since_gc.reset();
m_last_conflict.reset();
m_last_propagation.reset();
@ -161,6 +163,8 @@ namespace sat {
m_phase[v] = src.m_phase[v];
m_best_phase[v] = src.m_best_phase[v];
m_prev_phase[v] = src.m_prev_phase[v];
m_phase_birthdate[v] = src.m_phase_birthdate[v];
m_best_phase_birthdate[v] = src.m_best_phase_birthdate[v];
// inherit activity:
m_activity[v] = src.m_activity[v];
@ -267,6 +271,8 @@ namespace sat {
m_phase[v] = false;
m_best_phase[v] = false;
m_prev_phase[v] = false;
m_phase_birthdate[v] = 0;
m_best_phase_birthdate[v] = 0;
m_assigned_since_gc[v] = false;
m_last_conflict[v] = 0;
m_last_propagation[v] = 0;
@ -308,6 +314,8 @@ namespace sat {
m_phase.push_back(false);
m_best_phase.push_back(false);
m_prev_phase.push_back(false);
m_phase_birthdate.push_back(0);
m_best_phase_birthdate.push_back(0);
m_assigned_since_gc.push_back(false);
m_last_conflict.push_back(0);
m_last_propagation.push_back(0);
@ -645,6 +653,26 @@ namespace sat {
return 3*cls_allocator().get_allocation_size()/2 + memory::get_allocation_size() > memory::get_max_memory_size();
}
void solver::set_phase(literal l) {
if (l.var() >= num_vars())
return;
bool value = !l.sign();
set_phase(l.var(), value);
set_best_phase(l.var(), value);
}
void solver::set_phase(bool_var v, bool value) {
if (m_phase[v] != value)
m_phase_birthdate[v] = m_stats.m_conflicts;
m_phase[v] = value;
}
void solver::set_best_phase(bool_var v, bool value) {
if (m_best_phase[v] != value)
m_best_phase_birthdate[v] = m_stats.m_conflicts;
m_best_phase[v] = value;
}
struct solver::cmp_activity {
solver& s;
cmp_activity(solver& s):s(s) {}
@ -896,7 +924,7 @@ namespace sat {
m_assignment[(~l).index()] = l_false;
bool_var v = l.var();
m_justification[v] = j;
m_phase[v] = !l.sign();
set_phase(v, !l.sign());
m_assigned_since_gc[v] = true;
m_trail.push_back(l);
@ -904,17 +932,17 @@ namespace sat {
case BH_VSIDS:
break;
case BH_CHB:
m_last_propagation[v] = m_stats.m_conflict;
m_last_propagation[v] = m_stats.m_conflicts;
break;
}
if (m_config.m_anti_exploration) {
uint64_t age = m_stats.m_conflict - m_canceled[v];
uint64_t age = m_stats.m_conflicts - m_canceled[v];
if (age > 0) {
double decay = pow(0.95, static_cast<double>(age));
set_activity(v, static_cast<unsigned>(m_activity[v] * decay));
// NB. MapleSAT does not update canceled.
m_canceled[v] = m_stats.m_conflict;
m_canceled[v] = m_stats.m_conflicts;
}
}
@ -1378,8 +1406,10 @@ namespace sat {
lbool r = m_local_search->check(_lits.size(), _lits.data(), nullptr);
auto const& mdl = m_local_search->get_model();
if (mdl.size() == m_best_phase.size()) {
for (unsigned i = 0; i < m_best_phase.size(); ++i)
m_best_phase[i] = l_true == mdl[i];
for (unsigned i = 0; i < m_best_phase.size(); ++i) {
bool is_true = l_true == mdl[i];
set_best_phase(i, is_true);
}
if (r == l_true) {
m_conflicts_since_restart = 0;
@ -1671,12 +1701,12 @@ namespace sat {
while (!m_case_split_queue.empty()) {
if (m_config.m_anti_exploration) {
next = m_case_split_queue.min_var();
auto age = m_stats.m_conflict - m_canceled[next];
auto age = m_stats.m_conflicts - m_canceled[next];
while (age > 0) {
set_activity(next, static_cast<unsigned>(m_activity[next] * pow(0.95, static_cast<double>(age))));
m_canceled[next] = m_stats.m_conflict;
m_canceled[next] = m_stats.m_conflicts;
next = m_case_split_queue.min_var();
age = m_stats.m_conflict - m_canceled[next];
age = m_stats.m_conflicts - m_canceled[next];
}
}
next = m_case_split_queue.next_var();
@ -1714,6 +1744,25 @@ namespace sat {
}
}
void solver::get_backbone_candidates(literal_vector& lits, unsigned max_num) {
struct candidate {
literal lit;
uint64_t age;
};
svector<candidate> cands;
uint64_t now = m_stats.m_conflicts;
for (bool_var v = 0; v < num_vars(); ++v) {
if (value(v) != l_undef || was_eliminated(v))
continue;
bool is_pos = guess(v);
cands.push_back({ literal(v, !is_pos), now - get_phase_birthdate(v) });
}
std::stable_sort(cands.begin(), cands.end(),
[](candidate const& a, candidate const& b) { return a.age > b.age; });
for (unsigned i = 0; i < cands.size() && i < max_num; ++i)
lits.push_back(cands[i].lit);
}
bool solver::decide() {
bool_var next;
lbool phase = l_undef;
@ -2145,8 +2194,9 @@ namespace sat {
for (bool_var v = 0; v < num; ++v) {
if (!was_eliminated(v)) {
m_model[v] = value(v);
m_phase[v] = value(v) == l_true;
m_best_phase[v] = value(v) == l_true;
bool is_true = value(v) == l_true;
set_phase(v, is_true);
set_best_phase(v, is_true);
}
}
TRACE(sat_mc_bug, m_mc.display(tout););
@ -2274,7 +2324,7 @@ namespace sat {
m_restart_logs++;
std::stringstream strm;
strm << "(sat.stats " << std::setw(6) << m_stats.m_conflict << " "
strm << "(sat.stats " << std::setw(6) << m_stats.m_conflicts << " "
<< std::setw(6) << m_stats.m_decision << " "
<< std::setw(4) << m_stats.m_restart
<< mk_stat(*this)
@ -2432,7 +2482,7 @@ namespace sat {
m_conflicts_since_init++;
m_conflicts_since_restart++;
m_conflicts_since_gc++;
m_stats.m_conflict++;
m_stats.m_conflicts++;
if (m_step_size > m_config.m_step_size_min)
m_step_size -= m_config.m_step_size_dec;
@ -2564,7 +2614,7 @@ namespace sat {
tout << "missed " << lit << "@" << lvl(lit) << "\n";);
CTRACE(sat, idx == 0, display(tout););
if (idx == 0)
IF_VERBOSE(0, verbose_stream() << "num-conflicts: " << m_stats.m_conflict << "\n");
IF_VERBOSE(0, verbose_stream() << "num-conflicts: " << m_stats.m_conflicts << "\n");
VERIFY(idx > 0);
idx--;
}
@ -2874,7 +2924,7 @@ namespace sat {
inc_activity(var);
break;
case BH_CHB:
m_last_conflict[var] = m_stats.m_conflict;
m_last_conflict[var] = m_stats.m_conflicts;
break;
default:
break;
@ -2915,14 +2965,15 @@ namespace sat {
for (unsigned i = head; i < sz; ++i) {
bool_var v = m_trail[i].var();
TRACE(forget_phase, tout << "forgetting phase of v" << v << "\n";);
m_phase[v] = m_rand() % 2 == 0;
bool value = m_rand() % 2 == 0;
set_phase(v, value);
}
if (is_sat_phase() && head >= m_best_phase_size) {
m_best_phase_size = head;
IF_VERBOSE(12, verbose_stream() << "sticky trail: " << head << "\n");
for (unsigned i = 0; i < head; ++i) {
bool_var v = m_trail[i].var();
m_best_phase[v] = m_phase[v];
set_best_phase(v, m_phase[v]);
}
set_has_new_best_phase(true);
}
@ -2971,23 +3022,30 @@ namespace sat {
void solver::do_rephase() {
switch (m_config.m_phase) {
case PS_ALWAYS_TRUE:
for (auto& p : m_phase) p = true;
for (unsigned i = 0; i < m_phase.size(); ++i)
set_phase(i, true);
break;
case PS_ALWAYS_FALSE:
for (auto& p : m_phase) p = false;
for (unsigned i = 0; i < m_phase.size(); ++i)
set_phase(i, false);
break;
case PS_FROZEN:
break;
case PS_BASIC_CACHING:
switch (m_rephase.count % 4) {
case 0:
for (auto& p : m_phase) p = (m_rand() % 2) == 0;
for (unsigned i = 0; i < m_phase.size(); ++i) {
bool value = (m_rand() % 2) == 0;
set_phase(i, value);
}
break;
case 1:
for (auto& p : m_phase) p = false;
for (unsigned i = 0; i < m_phase.size(); ++i)
set_phase(i, false);
break;
case 2:
for (auto& p : m_phase) p = !p;
for (unsigned i = 0; i < m_phase.size(); ++i)
set_phase(i, !m_phase[i]);
break;
default:
break;
@ -2995,18 +3053,21 @@ namespace sat {
break;
case PS_SAT_CACHING:
if (m_search_state == s_sat)
for (unsigned i = 0; i < m_phase.size(); ++i)
m_phase[i] = m_best_phase[i];
for (unsigned i = 0; i < m_phase.size(); ++i)
set_phase(i, m_best_phase[i]);
break;
case PS_RANDOM:
for (auto& p : m_phase) p = (m_rand() % 2) == 0;
for (unsigned i = 0; i < m_phase.size(); ++i) {
bool value = (m_rand() % 2) == 0;
set_phase(i, value);
}
break;
case PS_LOCAL_SEARCH:
if (m_search_state == s_sat) {
if (m_rand() % 2 == 0)
bounded_local_search();
for (unsigned i = 0; i < m_phase.size(); ++i)
m_phase[i] = m_best_phase[i];
for (unsigned i = 0; i < m_phase.size(); ++i)
set_phase(i, m_best_phase[i]);
}
break;
@ -3601,6 +3662,8 @@ namespace sat {
m_phase.shrink(v);
m_best_phase.shrink(v);
m_prev_phase.shrink(v);
m_phase_birthdate.shrink(v);
m_best_phase_birthdate.shrink(v);
m_assigned_since_gc.shrink(v);
m_simplifier.reset_todos();
}
@ -3644,7 +3707,7 @@ namespace sat {
SASSERT(value(v) == l_undef);
m_case_split_queue.unassign_var_eh(v);
if (m_config.m_anti_exploration) {
m_canceled[v] = m_stats.m_conflict;
m_canceled[v] = m_stats.m_conflicts;
}
}
m_trail.shrink(old_sz);
@ -3812,7 +3875,7 @@ namespace sat {
double multiplier = m_config.m_reward_offset * (is_sat ? m_config.m_reward_multiplier : 1.0);
for (unsigned i = qhead; i < m_trail.size(); ++i) {
auto v = m_trail[i].var();
auto d = m_stats.m_conflict - m_last_conflict[v] + 1;
auto d = m_stats.m_conflicts - m_last_conflict[v] + 1;
if (d == 0) d = 1;
auto reward = multiplier / d;
auto activity = m_activity[v];
@ -4745,7 +4808,7 @@ namespace sat {
st.update("sat mk var", m_mk_var);
st.update("sat gc clause", m_gc_clause);
st.update("sat del clause", m_del_clause);
st.update("sat conflicts", m_conflict);
st.update("sat conflicts", m_conflicts);
st.update("sat decisions", m_decision);
st.update("sat propagations 2ary", m_bin_propagate);
st.update("sat propagations 3ary", m_ter_propagate);

View file

@ -60,7 +60,7 @@ namespace sat {
unsigned m_mk_bin_clause;
unsigned m_mk_ter_clause;
unsigned m_mk_clause;
unsigned m_conflict;
unsigned m_conflicts;
unsigned m_propagate;
unsigned m_bin_propagate;
unsigned m_ter_propagate;
@ -148,6 +148,8 @@ namespace sat {
bool_vector m_phase;
bool_vector m_best_phase;
bool_vector m_prev_phase;
svector<uint64_t> m_phase_birthdate;
svector<uint64_t> m_best_phase_birthdate;
bool m_new_best_phase = false;
svector<char> m_assigned_since_gc;
search_state m_search_state;
@ -373,12 +375,18 @@ namespace sat {
bool was_eliminated(bool_var v) const { return m_eliminated[v]; }
void set_eliminated(bool_var v, bool f) override;
bool was_eliminated(literal l) const { return was_eliminated(l.var()); }
void set_phase(literal l) override { if (l.var() < num_vars()) m_best_phase[l.var()] = m_phase[l.var()] = !l.sign(); }
void set_phase(literal l) override;
void set_phase(bool_var v, bool value);
void set_best_phase(bool_var v, bool value);
bool get_phase(bool_var b) { return m_phase.get(b, false); }
bool get_best_phase(bool_var b) { return m_best_phase.get(b, false); }
uint64_t get_phase_birthdate(bool_var b) const { return m_phase_birthdate.get(b, 0); }
uint64_t get_best_phase_birthdate(bool_var b) const { return m_best_phase_birthdate.get(b, 0); }
void set_has_new_best_phase(bool b) { m_new_best_phase = b; }
bool has_new_best_phase() const { return m_new_best_phase; }
void move_to_front(bool_var b);
unsigned get_activity(bool_var v) const { return m_activity[v]; }
void get_backbone_candidates(literal_vector& lits, unsigned max_num);
unsigned scope_lvl() const { return m_scope_lvl; }
unsigned search_lvl() const { return m_search_lvl; }
bool at_search_lvl() const { return m_scope_lvl == m_search_lvl; }
@ -440,6 +448,8 @@ namespace sat {
void set_par(parallel* p, unsigned id);
bool canceled() { return !m_rlimit.inc(); }
config const& get_config() const { return m_config; }
void set_max_conflicts(unsigned n) { m_config.m_max_conflicts = n; }
unsigned get_max_conflicts() const { return m_config.m_max_conflicts; }
void set_drat(bool d) { m_config.m_drat = d; }
drat& get_drat() { return m_drat; }
drat* get_drat_ptr() { return &m_drat; }

View file

@ -27,7 +27,6 @@ Notes:
#include "solver/tactic2solver.h"
#include "solver/parallel_params.hpp"
#include "solver/parallel_tactical.h"
#include "solver/parallel_tactical2.h"
#include "tactic/tactical.h"
#include "tactic/aig/aig_tactic.h"
#include "tactic/core/propagate_values_tactic.h"
@ -391,6 +390,15 @@ public:
if (m_preprocess) m_preprocess->collect_statistics(st);
m_solver.collect_statistics(st);
}
void set_max_conflicts(unsigned max_conflicts) override {
m_solver.set_max_conflicts(max_conflicts);
}
unsigned get_max_conflicts() const override {
return m_solver.get_max_conflicts();
}
void get_unsat_core(expr_ref_vector & r) override {
r.reset();
r.append(m_core.size(), m_core.data());
@ -405,6 +413,46 @@ public:
}
}
unsigned get_assign_level(expr* e) const override {
m.is_not(e, e);
sat::bool_var bv = m_map.to_bool_var(e);
return bv == sat::null_bool_var ? UINT_MAX : m_solver.lvl(bv);
}
bool is_relevant(expr* e) const override {
m.is_not(e, e);
sat::bool_var bv = m_map.to_bool_var(e);
if (bv == sat::null_bool_var)
return true;
auto* ext = dynamic_cast<euf::solver*>(m_solver.get_extension());
return !ext || ext->is_relevant(bv);
}
unsigned get_num_bool_vars() const override {
return m_solver.num_vars();
}
sat::bool_var get_bool_var(expr* e) const override {
m.is_not(e, e);
return m_map.to_bool_var(e);
}
expr* bool_var2expr(sat::bool_var v) const override {
return v < m_solver.num_vars() ? m_map.bool_var2expr(v) : nullptr;
}
lbool get_assignment(sat::bool_var v) const override {
return v < m_solver.num_vars() ? m_solver.value(v) : l_undef;
}
double get_activity(sat::bool_var v) const override {
return v < m_solver.num_vars() ? static_cast<double>(m_solver.get_activity(v)) : 0.0;
}
bool was_eliminated(sat::bool_var v) const override {
return v < m_solver.num_vars() && m_solver.was_eliminated(v);
}
expr_ref_vector get_trail(unsigned max_level) override {
expr_ref_vector result(m);
unsigned sz = m_solver.trail_size();
@ -482,6 +530,70 @@ public:
return fmls;
}
expr_ref cube_vsids(expr_ref_vector const& invalid_split_atoms) override {
if (!is_internalized()) {
lbool r = internalize_formulas();
if (r != l_true)
return expr_ref(m);
}
convert_internalized();
if (m_solver.inconsistent())
return expr_ref(m);
obj_hashtable<expr> invalid_split_atoms_set;
for (expr* e : invalid_split_atoms) {
expr* atom = e;
m.is_not(e, atom);
invalid_split_atoms_set.insert(atom);
}
expr_ref result(m);
double score = 0.0;
unsigned n = 0;
unsigned search_lvl = m_solver.search_lvl();
for (auto& kv : m_map) {
sat::bool_var v = kv.m_value;
if (was_eliminated(v))
continue;
if (get_assignment(v) != l_undef && m_solver.lvl(v) <= search_lvl)
continue;
expr* e = kv.m_key;
if (!e)
continue;
expr* atom = e;
m.is_not(e, atom);
if (invalid_split_atoms_set.contains(atom))
continue;
double new_score = get_activity(v);
if (new_score > score || !result || (new_score == score && m_solver.rand()(++n) == 0)) {
score = new_score;
result = e;
}
}
return result;
}
void get_backbone_candidates(vector<solver::scored_literal>& candidates, unsigned max_num) override {
if (!is_internalized()) {
lbool r = internalize_formulas();
if (r != l_true)
return;
}
convert_internalized();
sat::literal_vector lits;
m_solver.get_backbone_candidates(lits, max_num);
expr_ref_vector lit2expr(m);
lit2expr.resize(m_solver.num_vars() * 2);
m_map.mk_inv(lit2expr);
uint64_t now = m_solver.get_stats().m_conflicts;
for (sat::literal lit : lits) {
expr* e = lit2expr.get(lit.index());
if (!e)
continue;
candidates.push_back(scored_literal(m, e, static_cast<double>(now - m_solver.get_phase_birthdate(lit.var()))));
}
}
expr* congruence_next(expr* e) override { return e; }
expr* congruence_root(expr* e) override { return e; }
expr_ref congruence_explain(expr* a, expr* b) override { return expr_ref(m.mk_eq(a, b), m); }
@ -1186,7 +1298,5 @@ tactic * mk_psat_tactic(ast_manager& m, params_ref const& p) {
parallel_params pp(p);
if (pp.enable())
return mk_parallel_tactic(mk_inc_sat_solver(m, p, false), p);
if (pp.enable2())
return mk_parallel_tactic2(mk_inc_sat_solver(m, p, false), p);
return mk_sat_tactic(m);
}

View file

@ -49,6 +49,13 @@ sat::bool_var atom2bool_var::to_bool_var(expr * n) const {
return m_mapping[idx].m_value;
}
expr* atom2bool_var::bool_var2expr(sat::bool_var v) const {
for (auto const& kv : m_mapping)
if (kv.m_value == v)
return kv.m_key;
return nullptr;
}
struct collect_boolean_interface_proc {
struct visitor {
obj_hashtable<expr> & m_r;

View file

@ -29,6 +29,7 @@ public:
atom2bool_var(ast_manager & m):expr2var(m) {}
void insert(expr * n, sat::bool_var v) { expr2var::insert(n, v); }
sat::bool_var to_bool_var(expr * n) const;
expr* bool_var2expr(sat::bool_var v) const;
void mk_inv(expr_ref_vector & lit2expr) const;
void mk_var_inv(expr_ref_vector & var2expr) const;
// return true if the mapping contains uninterpreted atoms.

View file

@ -155,7 +155,7 @@ namespace bv {
void ackerman::propagate() {
auto* n = m_queue;
vv* k = nullptr;
unsigned num_prop = static_cast<unsigned>(s.s().get_stats().m_conflict * s.get_config().m_dack_factor);
unsigned num_prop = static_cast<unsigned>(s.s().get_stats().m_conflicts * s.get_config().m_dack_factor);
num_prop = std::min(num_prop, m_table.size());
for (unsigned i = 0; i < num_prop; ++i, n = k) {
k = n->next();

View file

@ -171,7 +171,7 @@ namespace euf {
SASSERT(ctx.s().at_base_lvl());
auto* n = m_queue;
inference* k = nullptr;
unsigned num_prop = static_cast<unsigned>(ctx.s().get_stats().m_conflict * ctx.m_config.m_dack_factor);
unsigned num_prop = static_cast<unsigned>(ctx.s().get_stats().m_conflicts * ctx.m_config.m_dack_factor);
num_prop = std::min(num_prop, m_table.size());
for (unsigned i = 0; i < num_prop; ++i, n = k) {
k = n->next();

View file

@ -118,6 +118,10 @@ namespace smt {
if (!m_setup.already_configured()) {
m_fparams.updt_params(p);
}
else {
// selected parameters are safe to update after initialization
m_fparams.m_max_conflicts = p.get_uint("max_conflicts", m_fparams.m_max_conflicts);
}
for (auto th : m_theory_set)
if (th)
th->updt_params();
@ -3652,6 +3656,13 @@ namespace smt {
}
}
void context::setup_for_parallel() {
// Native SMT parallel configures the parent context before cloning workers.
// context::copy then configures/internalizes each worker copy while
// preprocessing is still enabled.
setup_context(m_fparams.m_auto_config);
}
config_mode context::get_config_mode(bool use_static_features) const {
if (!m_fparams.m_auto_config)
return CFG_BASIC;

View file

@ -64,6 +64,7 @@ namespace smt {
class model_generator;
class context;
class kernel;
struct oom_exception : public z3_error {
oom_exception() : z3_error(ERR_MEMOUT) {}
@ -85,6 +86,7 @@ namespace smt {
friend class model_generator;
friend class lookahead;
friend class parallel;
friend class kernel;
public:
statistics m_stats;
@ -292,6 +294,10 @@ namespace smt {
return m_fparams;
}
smt_params const& get_fparams() const {
return m_fparams;
}
params_ref const & get_params() {
return m_params;
}
@ -452,6 +458,8 @@ namespace smt {
svector<double> const & get_activity_vector() const { return m_activity; }
double get_activity(bool_var v) const { return m_activity[v]; }
unsigned get_num_assignments() const { return m_stats.m_num_assignments; }
unsigned get_birthdate(bool_var v) const { return m_birthdate[v]; }
void set_activity(bool_var v, double act) { m_activity[v] = act; }
@ -538,6 +546,8 @@ namespace smt {
return m_scope_lvl == m_search_lvl;
}
void pop_to_search_level() { pop_to_search_lvl(); }
bool tracking_assumptions() const {
return !m_assumptions.empty() && m_search_lvl > m_base_lvl;
}
@ -1697,6 +1707,8 @@ namespace smt {
lbool setup_and_check(bool reset_cancel = true);
void setup_for_parallel();
void reduce_assertions();
bool resource_limits_exceeded();
@ -1913,5 +1925,3 @@ namespace smt {
std::ostream& operator<<(std::ostream& out, enode_pp const& p);
};

View file

@ -280,10 +280,22 @@ namespace smt {
smt_params_helper::collect_param_descrs(d);
}
void kernel::pop_to_base_level() {
m_imp->m_kernel.pop_to_base_lvl();
}
void kernel::set_preprocess(bool f) {
m_imp->m_kernel.get_fparams().m_preprocess = f;
}
context & kernel::get_context() {
return m_imp->m_kernel;
}
context const& kernel::get_context() const {
return m_imp->m_kernel;
}
void kernel::get_levels(ptr_vector<expr> const& vars, unsigned_vector& depth) {
m_imp->m_kernel.get_levels(vars, depth);
}

View file

@ -300,6 +300,10 @@ namespace smt {
*/
static void collect_param_descrs(param_descrs & d);
void pop_to_base_level();
void set_preprocess(bool f);
void register_on_clause(void* ctx, user_propagator::on_clause_eh_t& on_clause);
/**
@ -340,6 +344,6 @@ namespace smt {
\warning This method should not be used in new code.
*/
context & get_context();
context const& get_context() const;
};
};

View file

@ -25,7 +25,7 @@ Author:
#include "smt/smt_parallel.h"
#include "smt/smt_lookahead.h"
#include "solver/solver_preprocess.h"
#include "params/smt_parallel_params.hpp"
#include "solver/parallel_params.hpp"
#include <cmath>
#include <mutex>
@ -550,7 +550,7 @@ namespace smt {
if (m_ablate_backtracking) {
// Ablation: for each target, pass the entire path from root to that node
for (auto const& target : targets) {
if (m_search_tree.is_lease_canceled(target.leased_node, target.cancel_epoch))
if (m_search_tree.is_lease_canceled(target.leased_node))
continue;
// Reconstruct the full path from root to this target node
@ -626,7 +626,7 @@ namespace smt {
ctx->set_logic(p.ctx.m_setup.get_logic());
context::copy(p.ctx, *ctx, true);
ctx->pop_to_base_lvl();
ctx->get_fparams().m_preprocess = false;
ctx->get_fparams().m_preprocess = false; // avoid preprocessing lemmas that are exchanged
}
void parallel::core_minimizer_worker::cancel() {
@ -763,22 +763,25 @@ namespace smt {
if (m_config.m_global_backbones) {
bb_candidates local_candidates = find_backbone_candidates();
b.collect_backbone_candidates(m_l2g, local_candidates);
if (!m.inc())
bool lease_canceled = false;
if (!b.checkpoint_worker(id, lease, lease_canceled))
return;
if (lease_canceled) {
LOG_WORKER(1, " abandoning canceled lease\n");
continue;
}
}
lbool r = check_cube(cube);
if (b.lease_canceled(lease)) {
bool lease_canceled = false;
if (!b.checkpoint_worker(id, lease, lease_canceled))
return;
if (lease_canceled) {
LOG_WORKER(1, " abandoning canceled lease\n");
lease = {};
m.limit().dec_cancel();
continue;
}
if (!m.inc())
return;
switch (r) {
case l_undef: {
update_max_thread_conflicts();
@ -790,7 +793,6 @@ namespace smt {
if (!atom)
goto check_cube_start;
b.try_split(m_l2g, id, lease, atom, m_config.m_threads_max_conflicts);
lease = {};
simplify();
break;
}
@ -825,7 +827,6 @@ namespace smt {
b.backtrack(m_l2g, id, core_to_use, lease);
if (m_config.m_core_minimize)
b.enqueue_core_minimization(m_l2g, source, unsat_core);
lease = {};
if (m_config.m_share_conflicts)
b.collect_clause(m_l2g, id, mk_not(mk_and(unsat_core)));
@ -854,10 +855,10 @@ namespace smt {
m_num_initial_atoms = ctx->get_num_bool_vars();
ctx->get_fparams().m_preprocess = false; // avoid preprocessing lemmas that are exchanged
smt_parallel_params pp(p.ctx.m_params);
m_config.m_inprocessing = pp.inprocessing();
m_config.m_global_backbones = pp.num_global_bb_batch_threads() > 0 || pp.num_global_bb_fl_threads() > 0;
m_config.m_local_backbones = pp.local_backbones();
parallel_params pp(p.ctx.m_params);
m_config.m_inprocessing = false;
m_config.m_global_backbones = pp.num_bb_threads() > 0;
m_config.m_local_backbones = false;
m_config.m_core_minimize = pp.core_minimize();
m_config.m_ablate_backtracking = pp.ablate_backtracking();
@ -887,9 +888,9 @@ namespace smt {
ctx->pop_to_base_lvl();
m_shared_units_prefix = ctx->assigned_literals().size();
m_num_initial_atoms = ctx->get_num_bool_vars();
ctx->get_fparams().m_preprocess = false; // avoid preprocessing lemmas that are exchanged
smt_parallel_params pp(p.ctx.m_params);
m_use_failed_literal_test = pp.num_global_bb_fl_threads() > 0;
m_use_failed_literal_test = false;
}
parallel::bb_candidates parallel::worker::find_backbone_candidates(unsigned k) {
@ -1105,14 +1106,48 @@ namespace smt {
return r;
}
void parallel::batch_manager::release_lease_unlocked(unsigned worker_id, node* n) {
if (worker_id >= m_worker_leases.size())
void parallel::batch_manager::set_canceled_unlocked() {
if (m_state != state::is_running)
return;
auto &lease = m_worker_leases[worker_id];
if (!lease.leased_node || lease.leased_node != n)
cancel_background_threads();
}
void parallel::batch_manager::set_canceled() {
std::scoped_lock lock(mux);
set_canceled_unlocked();
}
void parallel::batch_manager::release_worker_lease_unlocked(unsigned worker_id, node_lease& lease) {
if (worker_id >= m_worker_leases.size()) {
lease = {};
return;
m_search_tree.dec_active_workers(lease.leased_node);
}
auto& stored_lease = m_worker_leases[worker_id];
if (!stored_lease.leased_node || stored_lease.leased_node != lease.leased_node) {
lease = {};
return;
}
bool cancel_signaled = stored_lease.cancel_signaled;
m_search_tree.dec_active_workers(stored_lease.leased_node);
stored_lease = {};
lease = {};
if (cancel_signaled)
p.m_workers[worker_id]->limit().dec_cancel();
}
bool parallel::batch_manager::attempt_release_canceled_lease_unlocked(unsigned worker_id, node_lease& lease) {
if (m_state != state::is_running || !lease.leased_node || worker_id >= m_worker_leases.size())
return false;
auto& stored_lease = m_worker_leases[worker_id];
if (stored_lease.leased_node != lease.leased_node)
return false;
if (!m_search_tree.is_lease_canceled(stored_lease.leased_node))
return false;
release_worker_lease_unlocked(worker_id, lease);
return true;
}
void parallel::batch_manager::cancel_closed_leases_unlocked(unsigned source_worker_id) {
@ -1124,7 +1159,7 @@ namespace smt {
// only cancel workers that currently hold a lease, whose lease is canceled,
// and haven't already been signaled (prevents multiple inc_cancel() for same lease)
if (lease.leased_node && !lease.cancel_signaled && m_search_tree.is_lease_canceled(lease.leased_node, lease.cancel_epoch)) {
if (lease.leased_node && !lease.cancel_signaled && m_search_tree.is_lease_canceled(lease.leased_node)) {
p.m_workers[worker_id]->cancel_lease();
m_worker_leases[worker_id].cancel_signaled = true;
}
@ -1132,7 +1167,7 @@ namespace smt {
}
void parallel::batch_manager::backtrack(ast_translation &l2g, unsigned worker_id, expr_ref_vector const &core,
node_lease const &lease) {
node_lease& lease) {
std::scoped_lock lock(mux);
vector<cube_config::literal> g_core;
for (auto c : core)
@ -1277,7 +1312,7 @@ namespace smt {
if (!g_core.empty()) {
collect_matching_targets_unlocked(source, g_core[0].get(), g_core, targets);
for (auto const& target : targets) {
if (!m_search_tree.is_lease_canceled(target.leased_node, target.cancel_epoch))
if (!m_search_tree.is_lease_canceled(target.leased_node))
m_search_tree.backtrack(target.leased_node, g_core);
}
}
@ -1331,7 +1366,7 @@ namespace smt {
for (node* t : matches) {
if (!t || t == source)
continue;
if (m_search_tree.is_lease_canceled(t, t->get_cancel_epoch()))
if (m_search_tree.is_lease_canceled(t))
continue;
// When source is provided, keep only external matches. Nodes in the
@ -1358,12 +1393,12 @@ namespace smt {
if (!is_highest_ancestor)
continue;
targets.push_back({ t, t->get_cancel_epoch() });
targets.push_back({t});
}
}
void parallel::batch_manager::backtrack_unlocked(ast_translation& l2g, unsigned worker_id, expr_ref_vector const& core,
node_lease const* lease, vector<node_lease> const* targets) {
node_lease* lease, vector<node_lease> const* targets) {
if (m_state != state::is_running)
return;
@ -1374,17 +1409,25 @@ namespace smt {
SASSERT(lease != nullptr || targets != nullptr);
bool did_backtrack = false;
if (lease && !m_search_tree.is_lease_canceled(lease->leased_node, lease->cancel_epoch)) {
// we close/backtrack regardless of whether this lease is stale or not, as long as the lease isn't canceled
// i.e. worker 1 splits this node, but then worker 2 determines UNSAT --> worker 2 is stale but we still close this node and backtrack
did_backtrack = true;
IF_VERBOSE(1, verbose_stream() << "Batch manager backtracking.\n");
release_lease_unlocked(worker_id, lease->leased_node);
m_search_tree.backtrack(lease->leased_node, g_core);
if (lease) {
if (!m_search_tree.is_lease_canceled(lease->leased_node)) {
// we close/backtrack regardless of whether this lease is stale or not, as long as the lease isn't canceled
// i.e. worker 1 splits this node, but then worker 2 determines UNSAT --> worker 2 is stale but we still close this node and backtrack
did_backtrack = true;
IF_VERBOSE(1, verbose_stream() << "Batch manager backtracking.\n");
node* leased_node = lease->leased_node;
release_worker_lease_unlocked(worker_id, *lease);
m_search_tree.backtrack(leased_node, g_core);
}
else {
// the lease was canceled by another worker. don't backtrack on this node with whatever new core we just found with this thread
// however, we do proceed to external targets, since the new code may have exposed new external targets we can close/backtrack
attempt_release_canceled_lease_unlocked(worker_id, *lease);
}
}
if (targets) {
for (auto const& target : *targets) {
if (m_search_tree.is_lease_canceled(target.leased_node, target.cancel_epoch))
if (m_search_tree.is_lease_canceled(target.leased_node))
continue;
did_backtrack = true;
@ -1410,37 +1453,59 @@ namespace smt {
}
void parallel::batch_manager::try_split(ast_translation &l2g, unsigned worker_id,
node_lease const &lease, expr *atom, unsigned effort) {
node_lease& lease, expr *atom, unsigned effort) {
std::scoped_lock lock(mux);
if (m_state != state::is_running)
return;
if (m_search_tree.is_lease_canceled(lease.leased_node, lease.cancel_epoch))
if (m_search_tree.is_lease_canceled(lease.leased_node)) {
attempt_release_canceled_lease_unlocked(worker_id, lease);
return;
}
expr_ref lit(m), nlit(m);
lit = l2g(atom);
nlit = mk_not(m, lit);
bool did_split = m_search_tree.try_split(lease.leased_node, lease.cancel_epoch, lit, nlit, effort);
node* leased_node = lease.leased_node;
VERIFY(!leased_node->path_contains_atom(lit));
VERIFY(!leased_node->path_contains_atom(nlit));
bool did_split = m_search_tree.try_split(leased_node, lit, nlit, effort);
release_lease_unlocked(worker_id, lease.leased_node);
release_worker_lease_unlocked(worker_id, lease);
if (did_split) {
++m_stats.m_num_cubes;
m_stats.m_max_cube_depth = std::max(m_stats.m_max_cube_depth, lease.leased_node->depth() + 1);
m_stats.m_max_cube_depth = std::max(m_stats.m_max_cube_depth, leased_node->depth() + 1);
IF_VERBOSE(1, verbose_stream() << "Batch manager splitting on literal: " << mk_bounded_pp(lit, m, 3) << "\n");
}
}
void parallel::batch_manager::release_lease(unsigned worker_id, node_lease const &lease) {
bool parallel::batch_manager::checkpoint_worker(unsigned worker_id, node_lease& lease, bool& lease_canceled) {
std::scoped_lock lock(mux);
release_lease_unlocked(worker_id, lease.leased_node);
lease_canceled = false;
SASSERT(worker_id < p.m_workers.size());
if (attempt_release_canceled_lease_unlocked(worker_id, lease)) {
lease_canceled = true;
return true;
}
if (p.m_workers[worker_id]->limit().inc())
return true;
if (attempt_release_canceled_lease_unlocked(worker_id, lease)) {
lease_canceled = true;
return true;
}
set_canceled_unlocked();
return false;
}
bool parallel::batch_manager::lease_canceled(node_lease const &lease) {
std::scoped_lock lock(mux);
return m_state == state::is_running && m_search_tree.is_lease_canceled(lease.leased_node, lease.cancel_epoch);
return m_state == state::is_running && m_search_tree.is_lease_canceled(lease.leased_node);
}
void parallel::batch_manager::collect_clause(ast_translation &l2g, unsigned source_worker_id, expr *clause) {
@ -1745,7 +1810,6 @@ namespace smt {
IF_VERBOSE(2, m_search_tree.display(verbose_stream()); verbose_stream() << "\n";);
lease.leased_node = t;
lease.cancel_epoch = t->get_cancel_epoch();
if (id >= m_worker_leases.size())
m_worker_leases.resize(id + 1);
m_worker_leases[id] = lease;
@ -1779,8 +1843,9 @@ namespace smt {
m_worker_leases.reset();
m_worker_leases.resize(p.m_workers.size());
smt_parallel_params pp(p.ctx.m_params);
parallel_params pp(p.ctx.m_params);
m_ablate_backtracking = pp.ablate_backtracking();
m_canceled = false;
}
void parallel::batch_manager::collect_statistics(::statistics &st) const {
@ -1794,19 +1859,14 @@ namespace smt {
}
lbool parallel::operator()(expr_ref_vector const &asms) {
smt_parallel_params pp(ctx.m_params);
unsigned num_global_bb_batch_threads = pp.num_global_bb_batch_threads();
parallel_params pp(ctx.m_params);
unsigned num_global_bb_batch_threads = pp.num_bb_threads();
if (num_global_bb_batch_threads > 2)
throw default_exception("smt_parallel.num_global_bb_batch_threads must be 0, 1, or 2");
throw default_exception("parallel.num_bb_threads must be 0, 1, or 2");
unsigned num_workers = std::min((unsigned)std::thread::hardware_concurrency(), ctx.get_fparams().m_threads);
unsigned num_sls_threads = (pp.sls() ? 1 : 0);
unsigned num_sls_threads = 0;
unsigned num_core_min_threads = (pp.core_minimize() ? 1 : 0);
unsigned num_global_bb_fl_threads = pp.num_global_bb_fl_threads();
if (num_global_bb_fl_threads > 2)
throw default_exception("smt_parallel.num_global_bb_fl_threads must be 0, 1, or 2");
if (num_global_bb_fl_threads > 0 && num_global_bb_batch_threads > 0)
throw default_exception("smt_parallel.num_global_bb_fl_threads and smt_parallel.num_global_bb_batch_threads cannot both be enabled");
unsigned num_global_bb_threads = num_global_bb_fl_threads > 0 ? num_global_bb_fl_threads : num_global_bb_batch_threads;
unsigned num_global_bb_threads = num_global_bb_batch_threads;
unsigned total_threads = num_workers + num_sls_threads + num_core_min_threads + num_global_bb_threads;
IF_VERBOSE(1, verbose_stream() << "Parallel SMT with " << total_threads << " threads\n";);
@ -1856,18 +1916,52 @@ namespace smt {
<< m_global_backbones_workers.size() << " global backbone threads.\n";);
m_batch_manager.initialize(num_global_bb_threads);
auto safe_run = [&](auto&& run_fn, reslimit& lim) {
try {
run_fn();
if (lim.is_canceled())
m_batch_manager.set_canceled();
} catch (z3_error &err) {
IF_VERBOSE(0, verbose_stream() << "Exception in parallel solver: " << err.what() << "\n");
if (!lim.is_canceled())
m_batch_manager.set_exception(err.error_code());
else
m_batch_manager.set_canceled();
} catch (z3_exception &ex) {
IF_VERBOSE(0, verbose_stream() << "Exception in parallel solver: " << ex.what() << "\n");
if (!lim.is_canceled() && !is_cancellation_exception(ex.what()))
m_batch_manager.set_exception(ex.what());
else
m_batch_manager.set_canceled();
} catch (...) {
IF_VERBOSE(0, verbose_stream() << "Unknown exception in parallel solver\n");
if (!lim.is_canceled())
m_batch_manager.set_exception("unknown exception");
else
m_batch_manager.set_canceled();
}
};
// Launch threads
vector<std::thread> threads(total_threads);
unsigned thread_idx = 0;
for (auto* w : m_workers)
threads[thread_idx++] = std::thread([&, w]() { w->run(); });
threads[thread_idx++] = std::thread([w, &safe_run]() {
safe_run([w]() { w->run(); }, w->limit());
});
if (m_sls_worker)
threads[thread_idx++] = std::thread([&]() { m_sls_worker->run(); });
threads[thread_idx++] = std::thread([this, &safe_run]() {
safe_run([this]() { m_sls_worker->run(); }, m_sls_worker->limit());
});
if (m_core_minimizer_worker)
threads[thread_idx++] = std::thread([&]() { m_core_minimizer_worker->run(); });
threads[thread_idx++] = std::thread([this, &safe_run]() {
safe_run([this]() { m_core_minimizer_worker->run(); }, m_core_minimizer_worker->limit());
});
for (auto* w : m_global_backbones_workers)
threads[thread_idx++] = std::thread([&, w]() { w->run(); });
threads[thread_idx++] = std::thread([w, &safe_run]() {
safe_run([w]() { w->run(); }, w->limit());
});
// Wait for all threads to finish

View file

@ -32,6 +32,13 @@ namespace smt {
struct cube_config {
using literal = expr_ref;
static bool literal_is_null(expr_ref const& l) { return l == nullptr; }
static bool same_atom(expr_ref const& a, expr_ref const& b) {
expr* atom_a = a.get();
expr* atom_b = b.get();
a.get_manager().is_not(atom_a, atom_a);
b.get_manager().is_not(atom_b, atom_b);
return atom_a == atom_b;
}
static std::ostream& display_literal(std::ostream& out, expr_ref const& l) { return out << mk_bounded_pp(l, l.get_manager()); }
};
@ -145,7 +152,11 @@ namespace smt {
w->cancel();
}
std::atomic<bool> m_canceled = false;
void cancel_background_threads() {
if (m_canceled.exchange(true))
return; // already canceled
cancel_workers();
cancel_sls_worker();
if (!p.m_global_backbones_workers.empty()) {
@ -171,9 +182,11 @@ namespace smt {
}
void backtrack_unlocked(ast_translation& l2g, unsigned worker_id, expr_ref_vector const& core,
node_lease const* lease = nullptr, vector<node_lease> const* targets = nullptr);
node_lease* lease = nullptr, vector<node_lease> const* targets = nullptr);
void collect_clause_unlocked(ast_translation &l2g, unsigned source_worker_id, expr *clause);
void release_lease_unlocked(unsigned worker_id, node* n);
void set_canceled_unlocked();
void release_worker_lease_unlocked(unsigned worker_id, node_lease& lease);
bool attempt_release_canceled_lease_unlocked(unsigned worker_id, node_lease& lease);
void cancel_closed_leases_unlocked(unsigned source_worker_id);
void collect_matching_targets_unlocked(node* source, expr* lit, vector<cube_config::literal> const& core,
vector<node_lease>& targets);
@ -187,6 +200,7 @@ namespace smt {
void set_unsat(ast_translation& l2g, expr_ref_vector const& unsat_core);
void set_sat(ast_translation& l2g, model& m);
void set_canceled();
void set_exception(std::string const& msg);
void set_exception(unsigned error_code);
void collect_statistics(::statistics& st) const;
@ -210,14 +224,14 @@ namespace smt {
}
bool get_cube(ast_translation& g2l, unsigned id, expr_ref_vector& cube, bool is_first_run, node_lease& lease);
void backtrack(ast_translation& l2g, unsigned worker_id, expr_ref_vector const& core, node_lease const& lease);
void backtrack(ast_translation& l2g, unsigned worker_id, expr_ref_vector const& core, node_lease& lease);
void enqueue_core_minimization(ast_translation& l2g, node* source, expr_ref_vector const& core);
bool wait_for_core_min_job(ast_translation& g2l, node*& source,
expr_ref_vector& core, reslimit& lim);
void publish_minimized_core(ast_translation& l2g, expr_ref_vector const& asms, node* source,
unsigned original_core_size, expr_ref_vector const& minimized_core);
void try_split(ast_translation& l2g, unsigned worker_id, node_lease const& lease, expr* atom, unsigned effort);
void release_lease(unsigned worker_id, node_lease const& lease);
void try_split(ast_translation& l2g, unsigned worker_id, node_lease& lease, expr* atom, unsigned effort);
bool checkpoint_worker(unsigned worker_id, node_lease& lease, bool& lease_canceled);
bool lease_canceled(node_lease const& lease);
void collect_clause(ast_translation& l2g, unsigned source_worker_id, expr* clause);

View file

@ -22,12 +22,15 @@ Notes:
#include "ast/for_each_expr.h"
#include "ast/ast_pp.h"
#include "ast/func_decl_dependencies.h"
#include "smt/smt_context.h"
#include "smt/smt_kernel.h"
#include "params/smt_params.h"
#include "params/smt_params_helper.hpp"
#include "solver/solver_na2as.h"
#include "solver/mus.h"
#include <algorithm>
namespace {
class smt_solver : public solver_na2as {
@ -61,6 +64,7 @@ namespace {
smt_params m_smt_params;
smt::kernel m_context;
cuber* m_cuber;
random_gen m_rand;
symbol m_logic;
bool m_minimizing_core;
bool m_core_extend_patterns;
@ -84,16 +88,19 @@ namespace {
updt_params(p);
}
solver * translate(ast_manager & m, params_ref const & p) override {
ast_translation translator(get_manager(), m);
solver * translate(ast_manager & target, params_ref const & p) override {
ast_translation translator(get_manager(), target);
params_ref init;
init.copy(get_params());
init.copy(p);
smt_solver * result = alloc(smt_solver, m, p, m_logic);
smt_solver* result = alloc(smt_solver, target, init, m_logic);
smt::kernel::copy(m_context, result->m_context, true);
if (mc0())
if (mc0())
result->set_model_converter(mc0()->translate(translator));
for (auto & [k, v] : m_name2assertion) {
for (auto& [k, v] : m_name2assertion) {
expr* val = translator(k);
expr* key = translator(v);
result->assert_expr(val, key);
@ -212,6 +219,97 @@ namespace {
return m_context.get_trail(max_level);
}
expr_ref_vector get_assigned_literals() override {
expr_ref_vector result(m);
auto const& ctx = m_context.get_context();
for (auto lit : ctx.assigned_literals()) {
expr* atom = ctx.bool_var2expr(lit.var());
if (!atom)
continue;
result.push_back(lit.sign() ? m.mk_not(atom) : atom);
}
return result;
}
unsigned get_assign_level(expr* e) const override {
auto const& ctx = m_context.get_context();
get_manager().is_not(e, e);
if (!ctx.b_internalized(e))
return UINT_MAX;
return ctx.get_assign_level(ctx.get_bool_var(e));
}
bool is_relevant(expr* e) const override {
auto const& ctx = m_context.get_context();
get_manager().is_not(e, e);
return ctx.b_internalized(e) && ctx.is_relevant(e);
}
unsigned get_num_bool_vars() const override {
return m_context.get_context().get_num_bool_vars();
}
sat::bool_var get_bool_var(expr* e) const override {
auto const& ctx = m_context.get_context();
get_manager().is_not(e, e);
return ctx.b_internalized(e) ? ctx.get_bool_var(e) : sat::null_bool_var;
}
void pop_to_base_level() override {
m_context.pop_to_base_level();
}
void setup_for_parallel() override {
m_context.get_context().setup_for_parallel();
}
void set_preprocess(bool f) override {
m_context.set_preprocess(f);
}
void set_max_conflicts(unsigned max_conflicts) override {
auto& ctx = m_context.get_context();
ctx.get_fparams().m_max_conflicts = max_conflicts;
}
unsigned get_max_conflicts() const override {
return m_context.get_context().get_fparams().m_max_conflicts;
}
void get_backbone_candidates(vector<solver::scored_literal>& candidates, unsigned max_num) override {
ast_manager& m = get_manager();
auto& ctx = m_context.get_context();
unsigned curr_time = ctx.get_num_assignments();
vector<solver::scored_literal> all;
for (unsigned v = 0; v < ctx.get_num_bool_vars(); ++v) {
if (ctx.get_assignment(v) != l_undef && ctx.get_assign_level(v) == ctx.get_base_level())
continue;
expr* candidate = ctx.bool_var2expr(v);
if (!candidate)
continue;
auto const& d = ctx.get_bdata(v);
if (d.m_phase_available && !d.m_phase)
candidate = m.mk_not(candidate);
double age = static_cast<double>(curr_time - ctx.get_birthdate(v));
all.push_back(solver::scored_literal(m, candidate, age));
}
std::stable_sort(
all.begin(),
all.end(),
[](solver::scored_literal const& a, solver::scored_literal const& b) {
return a.score > b.score;
});
unsigned n = std::min<unsigned>(max_num, all.size());
for (unsigned i = 0; i < n; ++i)
candidates.push_back(all[i]);
}
void register_on_clause(void* ctx, user_propagator::on_clause_eh_t& on_clause) override {
m_context.register_on_clause(ctx, on_clause);
}
@ -368,6 +466,39 @@ namespace {
return lits;
}
expr_ref cube_vsids(expr_ref_vector const& invalid_split_atoms) override {
ast_manager& m = get_manager();
auto& ctx = m_context.get_context();
obj_hashtable<expr> invalid_split_atoms_set;
for (expr* e : invalid_split_atoms) {
expr* atom = e;
m.is_not(e, atom);
invalid_split_atoms_set.insert(atom);
}
expr_ref result(m);
double score = 0.0;
unsigned n = 0;
ctx.pop_to_search_level();
for (unsigned v = 0; v < ctx.get_num_bool_vars(); ++v) {
if (ctx.get_assignment(v) != l_undef)
continue;
expr* e = ctx.bool_var2expr(v);
if (!e)
continue;
expr* atom = e;
m.is_not(e, atom);
if (invalid_split_atoms_set.contains(atom))
continue;
double new_score = ctx.get_activity(v);
if (new_score > score || !result || (new_score == score && m_rand(++n) == 0)) {
score = new_score;
result = e;
}
}
return result;
}
struct collect_fds_proc {
ast_manager & m;
func_decl_set & m_fds;
@ -537,4 +668,3 @@ public:
solver_factory * mk_smt_solver_factory() {
return alloc(smt_solver_factory);
}

View file

@ -31,7 +31,6 @@ Notes:
#include "solver/solver.h"
#include "solver/mus.h"
#include "solver/parallel_tactical.h"
#include "solver/parallel_tactical2.h"
#include "solver/parallel_params.hpp"
#include <mutex>
@ -431,8 +430,6 @@ static tactic * mk_seq_smt_tactic(ast_manager& m, params_ref const & p) {
tactic * mk_parallel_smt_tactic(ast_manager& m, params_ref const& p) {
parallel_params pp(p);
if (pp.enable2())
return mk_parallel_tactic2(mk_smt_solver(m, p, symbol::null), p);
return mk_parallel_tactic(mk_smt_solver(m, p, symbol::null), p);
}
@ -440,8 +437,6 @@ tactic * mk_smt_tactic_core(ast_manager& m, params_ref const& p, symbol const& l
parallel_params pp(p);
if (pp.enable())
return mk_parallel_tactic(mk_smt_solver(m, p, logic), p);
if (pp.enable2())
return mk_parallel_tactic2(mk_smt_solver(m, p, logic), p);
return mk_seq_smt_tactic(m, p);
}
@ -450,7 +445,7 @@ tactic * mk_smt_tactic_core_using(ast_manager& m, bool auto_config, params_ref c
params_ref p = _p;
p.set_bool("auto_config", auto_config);
tactic *t = nullptr;
if (pp.enable() || pp.enable2())
if (pp.enable())
t = mk_parallel_smt_tactic(m, p);
else
t = mk_seq_smt_tactic(m, p);

View file

@ -5,7 +5,6 @@ z3_add_component(solver
combined_solver.cpp
mus.cpp
parallel_tactical.cpp
parallel_tactical2.cpp
simplifier_solver.cpp
slice_solver.cpp
smt_logics.cpp

View file

@ -4,8 +4,11 @@ def_module_params('parallel',
export=True,
params=(
('enable', BOOL, False, 'enable parallel solver by default on selected tactics (for QF_BV)'),
('enable2', BOOL, False, 'enable (experimental) parallel solver by default on selected tactics (for QF_BV)'),
('threads.max', UINT, 10000, 'caps maximal number of threads below the number of processors'),
('num_bb_threads', UINT, 2, 'run Janota-style chunking backbone worker threads; default is 2 (negative and positive mode), supported values are 0 (off), 1 (negative mode only) or 2 (negative and positive mode)'),
('core_minimize', BOOL, True, 'minimize unsat cores used for parallel cube backtracking'),
('ablate_backtracking', BOOL, False, 'ablation: pass entire cube as core instead of unsat core during backtracking'),
('cube.lookahead', BOOL, False, 'use lookahead cubing in the parallel solver; when false, use VSIDS activity to select one split literal'),
('conquer.batch_size', UINT, 100, 'number of cubes to batch together for fast conquer'),
('conquer.restart.max', UINT, 5, 'maximal number of restarts during conquer phase'),
('conquer.delay', UINT, 10, 'delay of cubes until applying conquer'),

File diff suppressed because it is too large Load diff

View file

@ -1,23 +1,25 @@
/*++
Copyright (c) 2017 Microsoft Corporation
Copyright (c) 2024 Microsoft Corporation
Module Name:
parallel_tactic.h
parallel_tactical.h
Abstract:
Parallel tactic in the style of Treengeling.
Parallel portfolio solver using the solver API.
Models the internals after smt/smt_parallel.cpp but operates
on generic solver objects instead of smt::context.
Author:
Nikolaj Bjorner (nbjorner) 2017-10-9
(based on smt_parallel.cpp and parallel_tactical.cpp)
--*/
#pragma once
class tactic;
class solver;
class params_ref;
tactic * mk_parallel_tactic(solver* s, params_ref const& p);

View file

@ -1,903 +0,0 @@
/*++
Copyright (c) 2024 Microsoft Corporation
Module Name:
parallel_tactical2.cpp
Abstract:
Parallel portfolio solver using the solver API.
Models the internals after smt/smt_parallel.cpp but operates on generic
solver objects (smt_solver, inc_sat_solver, etc.) via the solver interface
instead of accessing smt::context internals directly.
Key features compared to parallel_tactical.cpp:
- Search tree for coordinated non-chronological backtracking (from smt_parallel).
- Shared clause pool: learned conflict clauses are broadcast to all workers.
- Shared backbone/unit pool: base-level units propagated by one worker are
asserted as facts on every other worker's solver.
- Workers reuse their solver state across multiple cube checks, accumulating
learned clauses (same pattern as smt_parallel workers).
Key differences from smt_parallel:
- Uses the solver API throughout (translate, check_sat, get_trail, cube,
get_model, get_unsat_core, assert_expr, push, pop, updt_params, )
rather than accessing smt::context members directly.
- Works with any conforming solver implementation.
Cube path management follows the assumption-based pattern from smt_parallel:
- The worker's solver base assertion set is fixed at construction (the full
problem is translated into the worker's own ast_manager once).
- Shared clauses discovered by other workers are appended to the base set via
assert_expr at any time.
- The current cube path is passed as extra assumptions on every check_sat call,
so the solver can reuse learned clauses across different cube checks.
Split atom selection is performed by temporarily pushing the cube path onto
the solver, calling solver::cube(), retrieving the first proposed literal, and
then popping, so that the base state is preserved.
Author:
(based on smt_parallel.cpp by nbjorner / Ilana Shapiro, and
parallel_tactical.cpp by nbjorner / Miguel Neves)
--*/
#include "util/scoped_ptr_vector.h"
#include "util/uint_set.h"
#include "ast/ast_pp.h"
#include "ast/ast_ll_pp.h"
#include "ast/ast_util.h"
#include "ast/ast_translation.h"
#include "solver/solver.h"
#include "solver/parallel_tactical2.h"
#include "solver/parallel_params.hpp"
#include "solver/solver_preprocess.h"
#include "util/search_tree.h"
#include "tactic/tactic.h"
#include "tactic/tactical.h"
#include "solver/solver2tactic.h"
#include <cmath>
#include <mutex>
/* ------------------------------------------------------------------ */
/* Single-threaded stub */
/* ------------------------------------------------------------------ */
class non_parallel_tactic2 : public tactic {
public:
non_parallel_tactic2(solver*, params_ref const&) {}
char const* name() const override { return "parallel_tactic2"; }
void operator()(const goal_ref&, goal_ref_buffer&) override {
throw default_exception("parallel_tactic2 is disabled in single-threaded mode");
}
tactic* translate(ast_manager&) override { return nullptr; }
void cleanup() override {}
};
#ifdef SINGLE_THREAD
tactic* mk_parallel_tactic2(solver* s, params_ref const& p) {
return alloc(non_parallel_tactic2, s, p);
}
#else
#include <atomic>
#include <thread>
#include <condition_variable>
/* ------------------------------------------------------------------ */
/* Search-tree literal configuration */
/* ------------------------------------------------------------------ */
struct solver_cube_config {
using literal = expr_ref;
static bool literal_is_null(expr_ref const& l) { return l == nullptr; }
static std::ostream& display_literal(std::ostream& out, expr_ref const& l) {
if (l) return out << mk_bounded_pp(l, l.get_manager());
return out << "(null)";
}
};
/* ------------------------------------------------------------------ */
/* parallel_solver the core portfolio engine */
/* ------------------------------------------------------------------ */
class parallel_solver {
/* ---- forward declarations ---- */
class worker;
/* ---- node lease (mirrors smt_parallel) ---- */
struct node_lease {
search_tree::node<solver_cube_config>* leased_node = nullptr;
unsigned cancel_epoch = 0;
bool cancel_signaled = false;
};
/* ---- shared clause entry ---- */
struct shared_clause {
unsigned source_worker_id;
expr_ref clause;
};
/* ================================================================
* batch_manager
* Coordinates workers: distributes cubes, collects clauses/units,
* stores the final result (sat model / unsat core / exception).
* ================================================================ */
class batch_manager {
enum state {
is_running,
is_sat,
is_unsat,
is_exception_msg,
is_exception_code
};
struct stats {
unsigned m_num_cubes = 0;
unsigned m_max_cube_depth = 0;
unsigned m_backbones_found = 0;
};
ast_manager& m;
parallel_solver& p;
std::mutex mux;
state m_state = state::is_running;
stats m_stats;
search_tree::tree<solver_cube_config> m_search_tree;
vector<node_lease> m_worker_leases;
/* shared clause pool (guarded by mux) */
vector<shared_clause> m_shared_clause_trail;
obj_hashtable<expr> m_shared_clause_set;
/* shared backbone / unit pool (guarded by mux) */
obj_hashtable<expr> m_global_backbones;
/* result storage (guarded by mux) */
unsigned m_exception_code = 0;
std::string m_exception_msg;
model_ref m_model; /* sat model translated to m */
expr_ref_vector m_unsat_core; /* unsat core translated to m */
/* ---- cancellation helpers (called under mux) ---- */
void cancel_workers_unlocked() {
IF_VERBOSE(1, verbose_stream() << "par2: canceling workers\n");
for (auto* w : p.m_workers)
w->cancel();
}
void release_lease_unlocked(unsigned worker_id,
search_tree::node<solver_cube_config>* n) {
if (worker_id >= m_worker_leases.size()) return;
auto& lease = m_worker_leases[worker_id];
if (!lease.leased_node || lease.leased_node != n) return;
m_search_tree.dec_active_workers(lease.leased_node);
lease = {};
}
void cancel_closed_leases_unlocked(unsigned source_worker_id) {
unsigned n = std::min(m_worker_leases.size(), p.m_workers.size());
for (unsigned id = 0; id < n; ++id) {
if (id == source_worker_id) continue;
auto const& lease = m_worker_leases[id];
if (lease.leased_node && !lease.cancel_signaled &&
m_search_tree.is_lease_canceled(lease.leased_node, lease.cancel_epoch)) {
p.m_workers[id]->cancel_lease();
m_worker_leases[id].cancel_signaled = true;
}
}
}
void collect_clause_unlocked(ast_translation& l2g,
unsigned source_worker_id,
expr* clause) {
expr* g_clause = l2g(clause);
if (!m_shared_clause_set.contains(g_clause)) {
m_shared_clause_set.insert(g_clause);
shared_clause sc{source_worker_id, expr_ref(g_clause, m)};
m_shared_clause_trail.push_back(std::move(sc));
}
}
bool is_global_backbone_unlocked(ast_translation& l2g,
expr* bb_cand) {
expr_ref cand(l2g(bb_cand), m);
return m_global_backbones.contains(cand.get());
}
public:
batch_manager(ast_manager& m, parallel_solver& p)
: m(m), p(p),
m_search_tree(expr_ref(m)),
m_unsat_core(m) {}
/* ---- initialisation ---- */
void initialize(unsigned num_workers,
unsigned initial_max_thread_conflicts = 1000) {
m_state = state::is_running;
m_search_tree.reset();
m_search_tree.set_effort_unit(initial_max_thread_conflicts);
m_worker_leases.reset();
m_worker_leases.resize(num_workers);
m_shared_clause_trail.reset();
m_shared_clause_set.reset();
m_global_backbones.reset();
m_model = nullptr;
m_unsat_core.reset();
}
/* ---- result setters (called by workers, guarded by mux) ---- */
void set_sat(ast_translation& l2g, model& mdl) {
std::scoped_lock lock(mux);
IF_VERBOSE(1, verbose_stream() << "par2: batch_manager SAT\n");
if (m_state != state::is_running) return;
m_state = state::is_sat;
m_model = mdl.translate(l2g);
cancel_workers_unlocked();
}
void set_unsat(ast_translation& l2g,
expr_ref_vector const& core) {
std::scoped_lock lock(mux);
IF_VERBOSE(1, verbose_stream() << "par2: batch_manager UNSAT\n");
if (m_state != state::is_running) return;
m_state = state::is_unsat;
SASSERT(m_unsat_core.empty());
for (expr* c : core)
m_unsat_core.push_back(l2g(c));
cancel_workers_unlocked();
}
void set_exception(std::string const& msg) {
std::scoped_lock lock(mux);
IF_VERBOSE(1, verbose_stream() << "par2: batch_manager exception: " << msg << "\n");
if (m_state != state::is_running) return;
m_state = state::is_exception_msg;
m_exception_msg = msg;
cancel_workers_unlocked();
}
void set_exception(unsigned error_code) {
std::scoped_lock lock(mux);
if (m_state != state::is_running) return;
m_state = state::is_exception_code;
m_exception_code = error_code;
cancel_workers_unlocked();
}
/* ---- cube distribution (called by workers) ---- */
bool get_cube(ast_translation& g2l, unsigned id,
expr_ref_vector& cube, bool is_first_run,
node_lease& lease) {
std::scoped_lock lock(mux);
cube.reset();
if (m_search_tree.is_closed()) return false;
if (m_state != state::is_running) return false;
auto* t = is_first_run
? m_search_tree.activate_root()
: m_search_tree.activate_best_node();
if (!t) return false;
lease.leased_node = t;
lease.cancel_epoch = t->get_cancel_epoch();
if (id >= m_worker_leases.size())
m_worker_leases.resize(id + 1);
m_worker_leases[id] = lease;
/* build cube from path root → t */
for (auto* cur = t; cur; cur = cur->parent()) {
if (solver_cube_config::literal_is_null(cur->get_literal()))
break;
cube.push_back(expr_ref(g2l(cur->get_literal().get()), g2l.to()));
}
return true;
}
/* ---- backtrack on conflict (called by workers) ---- */
void backtrack(ast_translation& l2g, unsigned worker_id,
expr_ref_vector const& core,
node_lease const& lease) {
std::scoped_lock lock(mux);
if (m_state != state::is_running) return;
vector<solver_cube_config::literal> g_core;
for (auto c : core)
g_core.push_back(expr_ref(l2g(c), m));
if (!m_search_tree.is_lease_canceled(
lease.leased_node, lease.cancel_epoch)) {
release_lease_unlocked(worker_id, lease.leased_node);
m_search_tree.backtrack(lease.leased_node, g_core);
}
cancel_closed_leases_unlocked(worker_id);
IF_VERBOSE(2, m_search_tree.display(verbose_stream() << "\n"););
if (m_search_tree.is_closed()) {
IF_VERBOSE(1, verbose_stream() << "par2: search tree closed → UNSAT\n");
m_state = state::is_unsat;
for (auto& e : m_search_tree.get_core_from_root())
m_unsat_core.push_back(e.get());
cancel_workers_unlocked();
}
}
/* ---- try to split (called on undef) ---- */
void try_split(ast_translation& l2g, unsigned worker_id,
node_lease const& lease,
expr* atom, unsigned effort) {
std::scoped_lock lock(mux);
if (m_state != state::is_running) return;
if (m_search_tree.is_lease_canceled(
lease.leased_node, lease.cancel_epoch)) return;
expr_ref lit(m), nlit(m);
lit = l2g(atom);
nlit = mk_not(m, lit);
bool did_split = m_search_tree.try_split(
lease.leased_node, lease.cancel_epoch,
lit, nlit, effort);
release_lease_unlocked(worker_id, lease.leased_node);
if (did_split) {
++m_stats.m_num_cubes;
m_stats.m_max_cube_depth = std::max(
m_stats.m_max_cube_depth,
lease.leased_node->depth() + 1);
IF_VERBOSE(1, verbose_stream() << "par2: split on "
<< mk_bounded_pp(lit, m, 3) << "\n");
}
}
void release_lease(unsigned worker_id, node_lease const& lease) {
std::scoped_lock lock(mux);
release_lease_unlocked(worker_id, lease.leased_node);
}
bool lease_canceled(node_lease const& lease) {
std::scoped_lock lock(mux);
return m_state == state::is_running &&
m_search_tree.is_lease_canceled(
lease.leased_node, lease.cancel_epoch);
}
/* ---- clause sharing ---- */
void collect_clause(ast_translation& l2g,
unsigned source_worker_id,
expr* clause) {
std::scoped_lock lock(mux);
collect_clause_unlocked(l2g, source_worker_id, clause);
}
expr_ref_vector return_shared_clauses(ast_translation& g2l,
unsigned& worker_limit,
unsigned worker_id) {
std::scoped_lock lock(mux);
expr_ref_vector result(g2l.to());
for (unsigned i = worker_limit; i < m_shared_clause_trail.size(); ++i) {
if (m_shared_clause_trail[i].source_worker_id != worker_id)
result.push_back(g2l(m_shared_clause_trail[i].clause.get()));
}
worker_limit = m_shared_clause_trail.size();
return result;
}
/* ---- backbone / unit sharing ---- */
bool collect_global_backbone(ast_translation& l2g,
expr_ref const& backbone,
unsigned source_worker_id = UINT_MAX) {
std::scoped_lock lock(mux);
if (is_global_backbone_unlocked(l2g, backbone.get()))
return false;
expr_ref g_bb(l2g(backbone.get()), m);
m_global_backbones.insert(g_bb.get());
++m_stats.m_backbones_found;
IF_VERBOSE(2, verbose_stream() << "par2: new backbone "
<< mk_bounded_pp(g_bb, m, 3) << "\n");
/* share it as a unit clause so other workers pick it up */
collect_clause_unlocked(l2g, source_worker_id, backbone.get());
return true;
}
/* ---- result accessors ---- */
lbool get_result() const {
if (m.limit().is_canceled()) return l_undef;
switch (m_state) {
case state::is_running:
throw default_exception("par2: inconsistent end state");
case state::is_sat: return l_true;
case state::is_unsat: return l_false;
case state::is_exception_msg:
throw default_exception(m_exception_msg.c_str());
case state::is_exception_code:
throw z3_error(m_exception_code);
default:
UNREACHABLE();
return l_undef;
}
}
model_ref& get_model() { return m_model; }
expr_ref_vector const& get_unsat_core() const { return m_unsat_core; }
void collect_statistics(statistics& st) const {
st.update("par2-cubes", m_stats.m_num_cubes);
st.update("par2-cube-depth", m_stats.m_max_cube_depth);
st.update("par2-backbones", m_stats.m_backbones_found);
}
}; // class batch_manager
/* ================================================================
* worker
* Each worker owns a translated copy of the original solver plus
* its own ast_manager. Workers communicate only through the
* batch_manager (mutex-protected).
* ================================================================ */
class worker {
struct config {
unsigned m_threads_max_conflicts = 1000;
double m_max_conflict_mul = 1.5;
unsigned m_max_conflicts = UINT_MAX;
bool m_share_units = true;
bool m_share_conflicts = true;
unsigned m_max_cube_depth = 20;
};
unsigned id;
batch_manager& b;
ast_manager m; /* worker-local manager */
ref<solver> s; /* translated solver copy */
expr_ref_vector asms; /* translated assumptions */
ast_translation m_g2l, m_l2g; /* global↔local translations */
config m_config;
expr_mark m_known_units; /* units already shared by this worker */
unsigned m_shared_clause_limit = 0;
void update_max_conflicts() {
m_config.m_threads_max_conflicts = static_cast<unsigned>(
m_config.m_max_conflict_mul * m_config.m_threads_max_conflicts);
/* cap at the configured global maximum to prevent runaway cube checks */
if (m_config.m_threads_max_conflicts > m_config.m_max_conflicts)
m_config.m_threads_max_conflicts = m_config.m_max_conflicts;
}
/* Check the current cube (passed as additional assumptions).
* The solver's conflict budget is set via updt_params before
* each call so that long-running cubes are interrupted. */
lbool check_cube(expr_ref_vector const& cube) {
params_ref p;
p.set_uint("max_conflicts",
std::min(m_config.m_threads_max_conflicts,
m_config.m_max_conflicts));
s->updt_params(p);
expr_ref_vector combined(m);
combined.append(asms);
combined.append(cube);
IF_VERBOSE(2, verbose_stream() << "par2 worker " << id
<< ": checking cube of size " << cube.size() << "\n");
lbool r = l_undef;
try {
r = s->check_sat(combined);
}
catch (z3_error& err) {
if (!m.limit().is_canceled())
b.set_exception(err.error_code());
}
catch (z3_exception& ex) {
if (!m.limit().is_canceled())
b.set_exception(ex.what());
}
IF_VERBOSE(2, verbose_stream() << "par2 worker " << id
<< ": cube result " << r << "\n");
return r;
}
/* Assert shared clauses discovered by other workers into the
* base assertion set of this worker's solver. The solver
* automatically re-uses them on the next check_sat call. */
void collect_shared_clauses() {
expr_ref_vector nc = b.return_shared_clauses(
m_g2l, m_shared_clause_limit, id);
for (expr* e : nc) {
IF_VERBOSE(4, verbose_stream() << "par2 worker " << id
<< ": asserting shared clause "
<< mk_bounded_pp(e, m, 3) << "\n");
s->assert_expr(e);
}
}
/* Propagate any new base-level units (backbone literals) this
* worker has learned to the shared backbone pool.
*
* Uses solver::get_trail(0) which returns all literals
* propagated at decision level 0. */
void share_units() {
if (!m_config.m_share_units) return;
expr_ref_vector trail = s->get_trail(0);
for (expr* e : trail) {
/* get_trail may include ground terms; skip complex ones */
expr* atom = e;
m.is_not(e, atom);
if (!is_uninterp_const(atom)) continue;
if (m_known_units.is_marked(e)) continue;
m_known_units.mark(e);
expr_ref lit(e, m);
b.collect_global_backbone(m_l2g, lit, id);
}
}
/* Select a split atom using solver::cube() on a temporary
* solver state that includes the current cube path.
*
* We push the cube literals, call cube(), take the first
* literal, then pop to restore the base state. */
expr_ref get_split_atom(expr_ref_vector const& cube) {
if (cube.size() >= m_config.m_max_cube_depth)
return expr_ref(nullptr, m);
s->push();
for (expr* c : cube)
s->assert_expr(c);
expr_ref_vector vars(m);
expr_ref_vector c = s->cube(vars, UINT_MAX);
s->pop(1);
/* solver::cube() convention: an empty result means done; a result
* whose last element is true means the problem is trivially sat;
* a result whose last element is false means unsat was detected.
* In all other cases every element (including index 0) is a
* valid literal that can serve as a split atom. */
if (c.empty() || m.is_true(c.back()) || m.is_false(c.back()))
return expr_ref(nullptr, m);
return expr_ref(c.get(0), m);
}
public:
worker(unsigned id, parallel_solver& p,
solver& src, params_ref const& params,
expr_ref_vector const& src_asms)
: id(id), b(p.m_batch_manager),
asms(m), m_g2l(src.get_manager(), m), m_l2g(m, src.get_manager())
{
/* create translated solver copy */
s = src.translate(m, params);
/* translate assumptions */
for (expr* a : src_asms)
asms.push_back(m_g2l(a));
IF_VERBOSE(1, verbose_stream() << "par2: worker " << id
<< " created (" << asms.size() << " assumptions)\n");
}
void run() {
bool is_first_run = true;
node_lease lease;
expr_ref_vector cube(m);
while (true) {
if (!b.get_cube(m_g2l, id, cube, is_first_run, lease)) {
IF_VERBOSE(1, verbose_stream() << "par2 worker " << id
<< ": no more cubes\n");
return;
}
is_first_run = false;
collect_shared_clauses();
lbool r = check_cube(cube);
if (b.lease_canceled(lease)) {
IF_VERBOSE(1, verbose_stream() << "par2 worker " << id
<< ": lease canceled\n");
lease = {};
m.limit().dec_cancel();
continue;
}
if (!m.inc()) return;
switch (r) {
case l_undef: {
update_max_conflicts();
IF_VERBOSE(1, verbose_stream() << "par2 worker " << id
<< ": undef attempting split\n");
expr_ref atom = get_split_atom(cube);
if (atom) {
b.try_split(m_l2g, id, lease, atom.get(),
m_config.m_threads_max_conflicts);
}
else {
b.release_lease(id, lease);
}
if (m_config.m_share_units) share_units();
break;
}
case l_true: {
IF_VERBOSE(1, verbose_stream() << "par2 worker " << id
<< ": SAT\n");
model_ref mdl;
s->get_model(mdl);
if (mdl)
b.set_sat(m_l2g, *mdl);
return;
}
case l_false: {
IF_VERBOSE(1, verbose_stream() << "par2 worker " << id
<< ": UNSAT cube\n");
expr_ref_vector core(m);
s->get_unsat_core(core);
/* Filter to only cube literals (exclude base assumptions). */
expr_ref_vector cube_core(m);
for (expr* c : core) {
if (cube.contains(c))
cube_core.push_back(c);
}
/* If core contains none of the cube lits, the whole
* problem is UNSAT independent of the cube path. */
if (cube_core.empty()) {
b.set_unsat(m_l2g, core);
return;
}
b.backtrack(m_l2g, id, cube_core, lease);
if (m_config.m_share_conflicts) {
/* Share the negation of the cube-core conjunction
* as a learned clause: ¬(c cₙ) ¬c ¬cₙ */
expr_ref_vector neg_lits(m);
for (expr* c : cube_core)
neg_lits.push_back(mk_not(expr_ref(c, m)));
expr_ref clause(mk_or(neg_lits), m);
b.collect_clause(m_l2g, id, clause.get());
}
if (m_config.m_share_units) share_units();
break;
}
} // switch
} // while
} // run()
void cancel() {
m.limit().cancel();
}
void cancel_lease() {
m.limit().inc_cancel();
}
void collect_statistics(statistics& st) const {
s->collect_statistics(st);
}
reslimit& limit() { return m.limit(); }
}; // class worker
/* ---- members ---- */
ref<solver> m_solver;
ast_manager& m_manager;
params_ref m_params;
scoped_ptr_vector<worker> m_workers;
batch_manager m_batch_manager;
statistics m_stats;
public:
parallel_solver(solver* s, params_ref const& p)
: m_solver(s),
m_manager(s->get_manager()),
m_params(p),
m_batch_manager(s->get_manager(), *this) {}
/* Run the portfolio. Returns sat/unsat/undef.
*
* On sat: *mdl is populated (translated into m_manager).
* On unsat: *core is populated (translated into m_manager).
* asms: original external assumptions (in m_manager). */
lbool solve(expr_ref_vector const& asms,
model_ref& mdl,
expr_ref_vector& core) {
parallel_params pp(m_params);
unsigned num_threads = std::min(
static_cast<unsigned>(std::thread::hardware_concurrency()),
pp.threads_max());
if (num_threads < 2) num_threads = 2;
IF_VERBOSE(1, verbose_stream() << "par2: launching " << num_threads
<< " threads\n");
if (m_manager.has_trace_stream())
throw default_exception(
"parallel_tactic2 does not work with trace streams");
/* Build workers each gets a translated solver copy. */
m_workers.reset();
scoped_limits sl(m_manager.limit());
params_ref worker_params(m_params);
worker_params.set_bool("override_incremental", true);
for (unsigned i = 0; i < num_threads; ++i) {
auto* w = alloc(worker, i, *this, *m_solver, worker_params, asms);
m_workers.push_back(w);
sl.push_child(&(w->limit()));
}
m_batch_manager.initialize(num_threads);
/* Launch threads. */
vector<std::thread> threads;
for (auto* w : m_workers)
threads.push_back(std::thread([w]() { w->run(); }));
for (auto& t : threads)
t.join();
/* Collect per-worker statistics. */
for (auto* w : m_workers)
w->collect_statistics(m_stats);
m_batch_manager.collect_statistics(m_stats);
m_manager.limit().reset_cancel();
lbool result = m_batch_manager.get_result();
if (result == l_true)
mdl = m_batch_manager.get_model();
if (result == l_false) {
for (expr* c : m_batch_manager.get_unsat_core())
core.push_back(c);
}
m_workers.reset();
return result;
}
void collect_statistics(statistics& st) const {
st.copy(m_stats);
}
void reset_statistics() {
m_stats.reset();
}
}; // class parallel_solver
/* ------------------------------------------------------------------ */
/* parallel_tactic2 wraps parallel_solver as a tactic */
/* ------------------------------------------------------------------ */
class parallel_tactic2 : public tactic {
solver_ref m_solver;
ast_manager& m_manager;
params_ref m_params;
statistics m_stats;
public:
parallel_tactic2(solver* s, params_ref const& p)
: m_solver(s), m_manager(s->get_manager()), m_params(p) {}
char const* name() const override { return "parallel_tactic2"; }
void operator()(const goal_ref& g, goal_ref_buffer& result) override {
fail_if_proof_generation("parallel_tactic2", g);
ast_manager& m = g->m();
if (m.has_trace_stream())
throw default_exception(
"parallel_tactic2 does not work with trace streams");
/* Translate goal into a set of clauses + assumptions. */
solver* s = m_solver->translate(m, m_params);
expr_ref_vector clauses(m);
ptr_vector<expr> assumptions_raw;
obj_map<expr, expr*> bool2dep;
ref<generic_model_converter> fmc;
extract_clauses_and_dependencies(g, clauses, assumptions_raw,
bool2dep, fmc);
for (expr* cl : clauses)
s->assert_expr(cl);
expr_ref_vector asms(m);
asms.append(assumptions_raw.size(), assumptions_raw.data());
parallel_solver ps(s, m_params);
model_ref mdl;
expr_ref_vector core(m);
lbool is_sat = ps.solve(asms, mdl, core);
ps.collect_statistics(m_stats);
switch (is_sat) {
case l_true:
g->reset();
if (g->models_enabled() && mdl) {
if (fmc)
g->add(concat(fmc.get(), model2model_converter(mdl.get())));
else
g->add(model2model_converter(mdl.get()));
}
break;
case l_false: {
SASSERT(!g->proofs_enabled());
expr_dependency* lcore = nullptr;
proof* pr = nullptr;
if (!core.empty()) {
for (expr* c : core) {
expr* dep = nullptr;
if (bool2dep.find(c, dep))
lcore = m.mk_join(lcore, m.mk_leaf(dep));
}
}
g->assert_expr(m.mk_false(), pr, lcore);
break;
}
case l_undef:
if (!m.inc())
throw tactic_exception(Z3_CANCELED_MSG);
break;
}
result.push_back(g.get());
}
void cleanup() override {
m_stats.reset();
}
tactic* translate(ast_manager& m) override {
solver* s = m_solver->translate(m, m_params);
return alloc(parallel_tactic2, s, m_params);
}
void updt_params(params_ref const& p) override {
m_params.copy(p);
}
void collect_statistics(statistics& st) const override {
st.copy(m_stats);
}
void reset_statistics() override {
m_stats.reset();
}
}; // class parallel_tactic2
tactic* mk_parallel_tactic2(solver* s, params_ref const& p) {
return alloc(parallel_tactic2, s, p);
}
#endif /* !SINGLE_THREAD */

View file

@ -1,25 +0,0 @@
/*++
Copyright (c) 2024 Microsoft Corporation
Module Name:
parallel_tactical2.h
Abstract:
Parallel portfolio solver using the solver API.
Models the internals after smt/smt_parallel.cpp but operates
on generic solver objects instead of smt::context.
Author:
(based on smt_parallel.cpp and parallel_tactical.cpp)
--*/
#pragma once
class tactic;
class solver;
class params_ref;
tactic * mk_parallel_tactic2(solver* s, params_ref const& p);

View file

@ -22,6 +22,7 @@ Notes:
#include "solver/check_sat_result.h"
#include "solver/progress_callback.h"
#include "util/params.h"
#include "util/sat_literal.h"
class solver;
class model_converter;
@ -58,6 +59,13 @@ class solver : public check_sat_result, public user_propagator::core {
params_ref m_params;
symbol m_cancel_backup_file;
public:
struct scored_literal {
expr_ref lit;
double score = 0.0;
scored_literal(ast_manager& m, expr* e, double s): lit(e, m), score(s) {}
scored_literal(expr_ref const& e, double s): lit(e), score(s) {}
};
solver(ast_manager& m): check_sat_result(m) {}
/**
@ -247,7 +255,9 @@ public:
\brief extract a lookahead candidates for branching.
*/
virtual expr_ref_vector cube(expr_ref_vector& vars, unsigned backtrack_level) = 0;
virtual expr_ref_vector cube(expr_ref_vector& vars, unsigned backtrack_level=0) = 0;
virtual expr_ref cube_vsids(expr_ref_vector const&) { return expr_ref(m); }
/**
\brief retrieve congruence closure root.
@ -298,9 +308,34 @@ public:
expr_ref_vector get_non_units();
virtual expr_ref_vector get_trail(unsigned max_level) = 0; // { return expr_ref_vector(get_manager()); }
virtual expr_ref_vector get_assigned_literals() { return get_trail(UINT_MAX); }
virtual unsigned get_assign_level(expr* e) const { return UINT_MAX; }
virtual bool is_relevant(expr* e) const { return true; }
virtual unsigned get_num_bool_vars() const { return UINT_MAX; }
virtual sat::bool_var get_bool_var(expr* e) const { return sat::null_bool_var; }
virtual expr* bool_var2expr(sat::bool_var) const { return nullptr; }
virtual lbool get_assignment(sat::bool_var) const { return l_undef; }
virtual double get_activity(sat::bool_var) const { return 0.0; }
virtual bool was_eliminated(sat::bool_var) const { return false; }
virtual void pop_to_base_level() {}
virtual void setup_for_parallel() {}
virtual void set_preprocess(bool) {}
virtual void set_max_conflicts(unsigned max_conflicts) {
params_ref p;
p.set_uint("max_conflicts", max_conflicts);
updt_params(p);
}
virtual unsigned get_max_conflicts() const { return UINT_MAX; }
virtual void get_levels(ptr_vector<expr> const& vars, unsigned_vector& depth) = 0;
virtual void get_backbone_candidates(vector<scored_literal>&, unsigned) {}
class scoped_push {
solver& s;
bool m_nopop;
@ -328,4 +363,3 @@ typedef ref<solver> solver_ref;
inline std::ostream& operator<<(std::ostream& out, solver const& s) {
return s.display(out);
}

View file

@ -179,6 +179,21 @@ public:
return m_solver->get_trail(max_level);
}
void setup_for_parallel() override { m_solver->setup_for_parallel(); }
void set_max_conflicts(unsigned c) override { m_solver->set_max_conflicts(c); }
unsigned get_max_conflicts() const override { return m_solver->get_max_conflicts(); }
expr_ref_vector get_assigned_literals() override { return m_solver->get_assigned_literals(); }
unsigned get_assign_level(expr* e) const override { flush_assertions(); return m_solver->get_assign_level(e); }
bool is_relevant(expr* e) const override { flush_assertions(); return m_solver->is_relevant(e); }
unsigned get_num_bool_vars() const override { flush_assertions(); return m_solver->get_num_bool_vars(); }
sat::bool_var get_bool_var(expr* e) const override { flush_assertions(); return m_solver->get_bool_var(e); }
expr* bool_var2expr(sat::bool_var v) const override { return m_solver->bool_var2expr(v); }
lbool get_assignment(sat::bool_var v) const override { return m_solver->get_assignment(v); }
double get_activity(sat::bool_var v) const override { return m_solver->get_activity(v); }
bool was_eliminated(sat::bool_var v) const override { return m_solver->was_eliminated(v); }
expr_ref cube_vsids(expr_ref_vector const& invalid_split_atoms) override { flush_assertions(); return m_solver->cube_vsids(invalid_split_atoms); }
void get_backbone_candidates(vector<scored_literal>& candidates, unsigned max_num) override { flush_assertions(); m_solver->get_backbone_candidates(candidates, max_num); }
model_converter* external_model_converter() const {
return concat(mc0(), local_model_converter());
}

View file

@ -195,6 +195,21 @@ public:
return m_solver->get_trail(max_level);
}
void setup_for_parallel() override { m_solver->setup_for_parallel(); }
void set_max_conflicts(unsigned c) override { m_solver->set_max_conflicts(c); }
unsigned get_max_conflicts() const override { return m_solver->get_max_conflicts(); }
expr_ref_vector get_assigned_literals() override { return m_solver->get_assigned_literals(); }
unsigned get_assign_level(expr* e) const override { return m_solver->get_assign_level(e); }
bool is_relevant(expr* e) const override { return m_solver->is_relevant(e); }
unsigned get_num_bool_vars() const override { return m_solver->get_num_bool_vars(); }
sat::bool_var get_bool_var(expr* e) const override { return m_solver->get_bool_var(e); }
expr* bool_var2expr(sat::bool_var v) const override { return m_solver->bool_var2expr(v); }
lbool get_assignment(sat::bool_var v) const override { return m_solver->get_assignment(v); }
double get_activity(sat::bool_var v) const override { return m_solver->get_activity(v); }
bool was_eliminated(sat::bool_var v) const override { return m_solver->was_eliminated(v); }
expr_ref cube_vsids(expr_ref_vector const& invalid_split_atoms) override { return m_solver->cube_vsids(invalid_split_atoms); }
void get_backbone_candidates(vector<scored_literal>& candidates, unsigned max_num) override { m_solver->get_backbone_candidates(candidates, max_num); }
unsigned get_num_assertions() const override {
return m_solver->get_num_assertions();
}

View file

@ -107,6 +107,21 @@ public:
return m_solver->get_trail(max_level);
}
void setup_for_parallel() override { m_solver->setup_for_parallel(); }
void set_max_conflicts(unsigned c) override { m_solver->set_max_conflicts(c); }
unsigned get_max_conflicts() const override { return m_solver->get_max_conflicts(); }
expr_ref_vector get_assigned_literals() override { return m_solver->get_assigned_literals(); }
unsigned get_assign_level(expr* e) const override { flush_assertions(); return m_solver->get_assign_level(e); }
bool is_relevant(expr* e) const override { flush_assertions(); return m_solver->is_relevant(e); }
unsigned get_num_bool_vars() const override { flush_assertions(); return m_solver->get_num_bool_vars(); }
sat::bool_var get_bool_var(expr* e) const override { flush_assertions(); return m_solver->get_bool_var(e); }
expr* bool_var2expr(sat::bool_var v) const override { return m_solver->bool_var2expr(v); }
lbool get_assignment(sat::bool_var v) const override { return m_solver->get_assignment(v); }
double get_activity(sat::bool_var v) const override { return m_solver->get_activity(v); }
bool was_eliminated(sat::bool_var v) const override { return m_solver->was_eliminated(v); }
expr_ref cube_vsids(expr_ref_vector const& invalid_split_atoms) override { flush_assertions(); return m_solver->cube_vsids(invalid_split_atoms); }
void get_backbone_candidates(vector<scored_literal>& candidates, unsigned max_num) override { flush_assertions(); m_solver->get_backbone_candidates(candidates, max_num); }
model_converter* external_model_converter() const{
return concat(mc0(), local_model_converter());
}

View file

@ -43,8 +43,6 @@ Notes:
#include "sat/sat_solver/inc_sat_solver.h"
#include "sat/sat_solver/sat_smt_solver.h"
#include "ast/rewriter/bv_rewriter.h"
#include "solver/solver2tactic.h"
#include "solver/parallel_tactical.h"
#include "solver/parallel_params.hpp"
#include "params/tactic_params.hpp"
#include "parsers/smt2/smt2parser.h"

View file

@ -50,7 +50,6 @@ namespace search_tree {
unsigned m_effort_spent = 0;
unsigned m_round_max_effort = 0;
unsigned m_active_workers = 0;
unsigned m_cancel_epoch = 0;
public:
node(literal const &l, node *parent) : m_literal(l), m_parent(parent), m_status(status::open) {}
@ -68,9 +67,17 @@ namespace search_tree {
literal const &get_literal() const {
return m_literal;
}
bool path_contains_atom(literal const& l) const {
for (node const* n = this; n; n = n->parent())
if (!Config::literal_is_null(n->get_literal()) && Config::same_atom(n->get_literal(), l))
return true;
return false;
}
void split(literal const &a, literal const &b) {
SASSERT(!Config::literal_is_null(a));
SASSERT(!Config::literal_is_null(b));
VERIFY(!path_contains_atom(a));
VERIFY(!path_contains_atom(b));
if (m_status != status::active)
return;
SASSERT(!m_left);
@ -148,12 +155,6 @@ namespace search_tree {
m_round_max_effort = effort;
m_effort_spent += m_round_max_effort;
}
unsigned get_cancel_epoch() const {
return m_cancel_epoch;
}
void inc_cancel_epoch() {
++m_cancel_epoch;
}
};
template <typename Config> class tree {
@ -340,7 +341,6 @@ namespace search_tree {
void close(node<Config> *n, vector<literal> const &C) {
if (!n || n->get_status() == status::closed)
return;
n->inc_cancel_epoch();
n->set_status(status::closed);
n->set_core(C);
close(n->left(), C);
@ -444,8 +444,8 @@ namespace search_tree {
// On timeout, either expand the current leaf or reopen the node for a
// later revisit, depending on the tree-expansion heuristic.
bool try_split(node<Config> *n, unsigned cancel_epoch, literal const &a, literal const &b, unsigned effort) {
if (is_lease_canceled(n, cancel_epoch))
bool try_split(node<Config> *n, literal const &a, literal const &b, unsigned effort) {
if (is_lease_canceled(n))
return false;
// Record at most one effort contribution per concurrent round on this node.
@ -544,8 +544,8 @@ namespace search_tree {
n->dec_active_workers();
}
bool is_lease_canceled(node<Config>* n, unsigned cancel_epoch) const {
return !n || n->get_status() == status::closed || n->get_cancel_epoch() != cancel_epoch;
bool is_lease_canceled(node<Config>* n) const {
return !n || n->get_status() == status::closed;
}
vector<literal> const &get_core_from_root() const {