abc/src/sat/cadical/cadical_reduce.cpp

285 lines
8.8 KiB
C++
Raw Normal View History

2025-03-07 09:25:11 +01:00
#include "global.h"
2025-03-06 06:25:31 +01:00
#include "internal.hpp"
2025-03-07 09:25:11 +01:00
ABC_NAMESPACE_IMPL_START
2025-03-06 06:25:31 +01:00
namespace CaDiCaL {
/*------------------------------------------------------------------------*/
// Once in a while we reduce, e.g., we remove learned clauses which are
// supposed to be less useful in the future. This is done in increasing
// intervals, which has the effect of allowing more and more learned clause
// to be kept for a longer period. The number of learned clauses kept
// in memory corresponds to an upper bound on the 'space' of a resolution
// proof needed to refute a formula in proof complexity sense.
bool Internal::reducing () {
if (!opts.reduce)
return false;
if (!stats.current.redundant)
return false;
return stats.conflicts >= lim.reduce;
}
/*------------------------------------------------------------------------*/
// Even less regularly we are flushing all redundant clauses.
bool Internal::flushing () {
if (!opts.flush)
return false;
return stats.conflicts >= lim.flush;
}
/*------------------------------------------------------------------------*/
void Internal::mark_clauses_to_be_flushed () {
const int tier1limit = tier1[false];
const int tier2limit = max (tier1limit, tier2[false]);
for (const auto &c : clauses) {
if (!c->redundant)
continue; // keep irredundant
if (c->garbage)
continue; // already marked as garbage
if (c->reason)
continue; // need to keep reasons
const unsigned used = c->used;
if (used)
c->used--;
if (c->glue < tier1limit && used)
continue;
if (c->glue < tier2limit && used >= max_used - 1)
continue;
mark_garbage (c); // flush unused clauses
if (c->hyper)
stats.flush.hyper++;
else
stats.flush.learned++;
}
// No change to 'lim.kept{size,glue}'.
}
/*------------------------------------------------------------------------*/
// Clauses of larger glue or larger size are considered less useful.
//
// We also follow the observations made by the Glucose team in their
// IJCAI'09 paper and keep all low glue clauses limited by
// 'options.keepglue' (typically '2').
//
// In earlier versions we pre-computed a 64-bit sort key per clause and
// wrapped a pointer to the clause and the 64-bit sort key into a separate
// data structure for sorting. This was probably faster but awkward and
// so we moved back to a simpler scheme which also uses 'stable_sort'
// instead of 'rsort' below. Sorting here is not a hot-spot anyhow.
struct reduce_less_useful {
bool operator() (const Clause *c, const Clause *d) const {
if (c->glue > d->glue)
return true;
if (c->glue < d->glue)
return false;
return c->size > d->size;
}
};
// This function implements the important reduction policy. It determines
// which redundant clauses are considered not useful and thus will be
// collected in a subsequent garbage collection phase.
void Internal::mark_useless_redundant_clauses_as_garbage () {
// We use a separate stack for sorting candidates for removal. This uses
// (slightly) more memory but has the advantage to keep the relative order
// in 'clauses' intact, which actually due to using stable sorting goes
// into the candidate selection (more recently learned clauses are kept if
// they otherwise have the same glue and size).
vector<Clause *> stack;
const int tier1limit = tier1[false];
const int tier2limit = max (tier1limit, tier2[false]);
stack.reserve (stats.current.redundant);
for (const auto &c : clauses) {
if (!c->redundant)
continue; // Keep irredundant.
if (c->garbage)
continue; // Skip already marked.
if (c->reason)
continue; // Need to keep reasons.
const unsigned used = c->used;
if (used)
c->used--;
if (c->glue <= tier1limit && used)
continue;
if (c->glue <= tier2limit && used >= max_used - 1)
continue;
if (c->hyper) { // Hyper binary and ternary resolvents
2025-03-07 09:25:11 +01:00
CADICAL_assert (c->size <= 3); // are only kept for one reduce round
2025-03-06 06:25:31 +01:00
if (!used)
mark_garbage (c); // unless
continue; // used recently.
}
stack.push_back (c);
}
stable_sort (stack.begin (), stack.end (), reduce_less_useful ());
size_t target = 1e-2 * opts.reducetarget * stack.size ();
// This is defensive code, which I usually consider a bug, but here I am
// just not sure that using floating points in the line above is precise
// in all situations and instead of figuring that out, I just use this.
//
if (target > stack.size ())
target = stack.size ();
PHASE ("reduce", stats.reductions, "reducing %zd clauses %.0f%%", target,
percent (target, stats.current.redundant));
auto i = stack.begin ();
const auto t = i + target;
while (i != t) {
Clause *c = *i++;
LOG (c, "marking useless to be collected");
mark_garbage (c);
stats.reduced++;
}
lim.keptsize = lim.keptglue = 0;
const auto end = stack.end ();
for (i = t; i != end; i++) {
Clause *c = *i;
LOG (c, "keeping");
if (c->size > lim.keptsize)
lim.keptsize = c->size;
if (c->glue > lim.keptglue)
lim.keptglue = c->glue;
}
erase_vector (stack);
PHASE ("reduce", stats.reductions, "maximum kept size %d glue %d",
lim.keptsize, lim.keptglue);
}
/*------------------------------------------------------------------------*/
// If chronological backtracking produces out-of-order assigned units, then
// it is necessary to completely propagate them at the root level in order
// to derive all implied units. Otherwise the blocking literals in
2025-03-07 09:25:11 +01:00
// 'flush_watches' are messed up and CADICAL_assertion 'FW1' fails.
2025-03-06 06:25:31 +01:00
bool Internal::propagate_out_of_order_units () {
if (!level)
return true;
int oou = 0;
for (size_t i = control[1].trail; !oou && i < trail.size (); i++) {
const int lit = trail[i];
2025-03-07 09:25:11 +01:00
CADICAL_assert (val (lit) > 0);
2025-03-06 06:25:31 +01:00
if (var (lit).level)
continue;
LOG ("found out-of-order assigned unit %d", oou);
oou = lit;
}
if (!oou)
return true;
2025-03-07 09:25:11 +01:00
CADICAL_assert (opts.chrono || external_prop);
2025-03-06 06:25:31 +01:00
backtrack (0);
if (propagate ())
return true;
learn_empty_clause ();
return false;
}
/*------------------------------------------------------------------------*/
// reduction is scheduled with reduceint, reducetarget and reduceopt.
// with reduceopt=1 the number of learnt clauses scale with
// sqrt of conflicts times reduceint
// the scaling is the same as with reduceopt=0 (the classical default)
// however, the constants are different. To avoid this (and get roughly the
// same behaviour with reduceopt=0 and reduceopt=1) we need to scale the
// interval, namely (reduceint^2/2)
// Lastly, reduceopt=2 just replaces sqrt conflicts with log conflicts.
// The learnt clauses should not be bigger than
// 1/reducetarget * reduceint * function (conflicts)
// for function being log if reduceint=2 an sqrt otherwise.
// This is however only the theoretical target and second chance for
// tier2 clauses and very long lifespan of tier1 clauses (through used flag)
// make this behave differently.
// reduceinit shifts the curve to the right, increasing the number of
// clauses in the solver. This impact will decrease over time.
void Internal::reduce () {
START (reduce);
stats.reductions++;
report ('.', 1);
bool flush = flushing ();
if (flush)
stats.flush.count++;
if (!propagate_out_of_order_units ())
goto DONE;
mark_satisfied_clauses_as_garbage ();
protect_reasons ();
if (flush)
mark_clauses_to_be_flushed ();
else
mark_useless_redundant_clauses_as_garbage ();
garbage_collection ();
{
int64_t delta = opts.reduceint;
double factor = stats.reductions + 1;
if (opts.reduceopt ==
0) // adjust delta such this is the same as reduceopt=1
delta = delta * delta / 2;
else if (opts.reduceopt == 1) {
// this is the same as reduceopt=0 if reduceint = sqrt (reduceint) =
// 17
factor = sqrt ((double) stats.conflicts);
} else if (opts.reduceopt == 2)
// log scaling instead
factor = log ((double) stats.conflicts);
if (factor < 1)
factor = 1;
delta = delta * factor;
if (irredundant () > 1e5) {
delta *= log (irredundant () / 1e4) / log (10);
}
if (delta < 1)
delta = 1;
lim.reduce = stats.conflicts + delta;
PHASE ("reduce", stats.reductions,
"new reduce limit %" PRId64 " after %" PRId64 " conflicts",
lim.reduce, delta);
}
if (flush) {
PHASE ("flush", stats.flush.count, "new flush increment %" PRId64 "",
inc.flush);
inc.flush *= opts.flushfactor;
lim.flush = stats.conflicts + inc.flush;
PHASE ("flush", stats.flush.count, "new flush limit %" PRId64 "",
lim.flush);
}
last.reduce.conflicts = stats.conflicts;
DONE:
report (flush ? 'f' : '-');
STOP (reduce);
}
} // namespace CaDiCaL
2025-03-07 09:25:11 +01:00
ABC_NAMESPACE_IMPL_END