Lecture 9: Solving Constraints.
In the last lecture, we described how a Sketch
can be symbolically executed to generate a set of constraints.
In this lecture we discuss some of the techniques that are
used to solve those constraints.
The workhorse for solving constraints is the SAT solver,
which is able to take a boolean formula expressed in Conjunctive
Normal Form (CNF) and either generate a satisfying assignment
or prove that the constraints are unsatisfiable, i.e. that they
have no solution. Over the rest of this section,
we describe how the constraints from the previous lecture
are translated into boolean constraints in CNF form, and how
the SAT solver is able to solve them.
From high-level constraints to CNF
Lecture9:Slide5
In the boolean satisfiability literature, a
Literal is either a variable or its negation.
A
Clause is a disjunction (or) of literals.
A formula is said to be in Conjunctive Normal Form
if it consists of a conjunction (and) of clauses.
The CNF representation has a number of advantages.
A particularly important one is that we can turn an
arbitrary boolean formula into CNF format in polynomial
time. This is unlike Disjunctive Normal Form (DNF)
which may require exponential time to generate.
The basic approach for generating a CNF formula
from an arbitrary boolean formula is illustrated
by the figure. The approach is based on the observation
that a formula of the form $\wedge_i l_i \Rightarrow l_j$
is trivially converted into a CNF formula.
Given a boolean formula represented as a DAG, it is easy
to define for each node a set of implications that
relate the values of the input to the values
of the output. For example, given a node of the
form $t1 = h_0 \wedge h_1$, we can write out the
set of implications that define the relationship
between $t1$, $h_0$ and $h_1$, namely,
\[
\begin{array}{ccc}
h_0 \wedge h_1 \Rightarrow t_1 & \equiv & \bar{h_0} \vee \bar{h_1} \vee t_1\\
t_1 \Rightarrow h_0 & \equiv & \bar{t_1} \vee h_0\\
t_1 \Rightarrow h_1 & \equiv & \bar{t_1} \vee h_1
\end{array}
\]
In addition to Booleans, though, Sketch supports Integers, Floating point values,
bit-vectors, arrays and recursive datatypes.
Most of these types are supported through
one-hot encodings.
One-hot encoding in Sketch
Lecture9:Slide10
A One-hot encoding is essentially a unary encoding.
The basic idea is to have a separate indicator variable
for each possible value of a variable that indicates what the true value
is. For example, the figure illustrates a one-hot encoding where
two variables, each with three possible values are added together.
Each value is represented as a list of value, indicator variable pairs.
The result contains all possible values that can result from adding the original
two numbers, and the new indicator variables are now boolean combinations
of the indicator variables in the original values.
By default, every integer in sketch is represented using this one-hot encoding,
and when using the "
--fe-fpencoding TO_BACKEND
" flag, even
floating point values are stored using this encoding as well.
By default, even arrays are represented using this encoding.
Every entry in the array having one bit for every possible value
for that entry. Recursive data-types are also represented
using this kind of encoding; the details are beyond the scope
of this lecture, but can be found on a recent paper by
Inala et al
InalaPQLS17.
There are a number of advantages to this encoding, particularly
its simplicity and flexibility. Most importantly, the encoding
can also be extremely efficient in allowing the SAT solver to propagate
information about the possible values of a variable.
The major downside of this encoding is that it is very space inefficient.
As the number of possible values for a variable grows, so does the
number of bits required to represent it. This can be particularly problematic
for sketches that are heavy in arithmetic.
For those sketches, sketch provides an alternative solver
that does not rely on one-hot encodings, and which can be enabled
with the flag "
--slv-nativeints
".
Solving SAT problems
Lecture9:Slide12;
Lecture9:Slide13;
Lecture9:Slide14;
Lecture9:Slide15;
Lecture9:Slide16;
Lecture9:Slide17;
Lecture9:Slide18;
Lecture9:Slide19;
Lecture9:Slide20;
Lecture9:Slide21;
Lecture9:Slide22;
Lecture9:Slide23;
Lecture9:Slide24;
Lecture9:Slide25;
Lecture9:Slide26;
Lecture9:Slide27
Modern SAT solvers are based on the DPLL algorithm named
after Martin Davis, George Logemann, Donald Loveland and Hilary Putnam.
The algorithm performs a backtracking search over
the space of possible assignments to a boolean formula,
but the key idea of the algorithm is that every time it makes a choice
for the value of a variable, it propagates the logical implications of
that assignment. The process is illustrated in the figure. After
the variable $x_1$ is set, the clause $\bar{x_1} \vee x_7$
forces the value of $x_7$ to be true through
unit propagation.
As the assignment of variables and propagation of logical consequences
continues, eventually one of two things happen, either the solver
assignes values to all the variables and terminates, or it runs
into a contradiction as illustrated in the figure where the
assignment of false to $x_9$ implies that $x_4$ is both true
and false according to different clauses.
Modern SAT solvers improve on this basic algorithm in three important
ways. The first one, also illustrated in the figure is
conflict analysis.
Every time the solver arrives at a contradiction, it traces back to identify
a small set of assignments that led to that contradiction. In the case
of the example, we can lay the blame of the conflict on the assignment
to $x_1$, $x_5$, and $\bar{x_9}$. This explanation for the conflict is
summarized in a
conflict clause that prevents that bad assignment from
appearing again. The conflict clause is not unique; for example,
$\bar{x_7} \vee \bar{x_5} \vee x_9$ would also be an acceptable conflict, because
as we can see from the figure, an assignment with $x_7, x_5$ and $\bar{x_9}$
would also lead to the same contradiction we observed.
More formally, every time we observe a conflict, we can define a
conflict graph where each node $N$ corresponds to a variable,
and there is a directed edge $(n_1, n_2)$ connecting two nodes iff
the clause that forced $n_2$ to be set to a value includes
variable $n_1$. It is not hard to see that this conflict graph will
be a DAG. The source nodes will be the variables that were chosen arbitrarily,
and the inner nodes correspond to the assignments that were implied by those
arbitrary assignments. Every time a contradiction is reached, the conflict
graph can tell us what the possible conflict clauses are. Specifically,
any set of nodes in the graph that separates the decision variables
(the sources in the DAG) from the conflict will make a valid conflict clause.
Thus, in the example, the conflict graph tells us that another valid
conflict clause would be $\bar{x_6} \vee x_3 \vee x_9$.
This process of learning conflict clauses is termed
Conflict Driven Clause Learning (CDCL) and
was first proposed by Marques Silva and Sakallah in their
seminal paper on the GRASP SAT Solver
SilvaS96.
The second important improvement over the basic algorithm
in addition to CDCL is called
two literal watching,
which was first developed by Moskewicz, Madigan, Zhao,
Zhang and Malik in the Chaff SAT solver
MoskewiczMZZM01.
The key observation behind two-literal watching is that
while in principle, every time we set the value of a variable
we have to visit all the clauses that include that variable,
in practice, the only cases where a clause actually leads
to some action is when we set the second to last literal.
At that point, if all other literals have been set to false,
and this second to last literal is also set to false, then
the last remaining literal must be set to true for the clause
to be satisfied, and we get unit propagation.
So unit propagation will only happen when we set the
second to last variable. So the idea is for every clause,
we keep track of two literals that haven't been set.
As long as there are two literals that have not been set,
setting other literals in the clause will have no effect,
so there is no need to do anything. Only when one of the
two unwatched literals is set, then we check one of three
possibilities: (a) maybe some of the other literals
we were not watching was already set to true, in which
case the clause is already satisfied and there is nothing
else to do; (b) maybe there are other literals that have not
been set, in which case we just switch the literals
we currently watch so that we are again watching to unassigned
literals, or (c) these two literals are the last
unassigned literals and all others have been set to false,
in which case we do unit propagation. By watching only
two literals at a time, the solver saves an enormous
amount of memory trafic by only having to visit a small
fraction of the clauses every time an variable is assigned.
Finally, the third important optimization in a modern
SAT solver involves being careful (but not too careful)
in picking which variables to assign next. The point
about not being too careful is actually important. The
most popular heuristic is Variable State Independent Decaying Sum
(VSIDS), which is very simple, but also very fast, so even
though it is not too sophisticated, because it is very fast,
it allows us to search more efficiently overall.
The basic idea in VSIDS is to keep a score for every
variable that is additively bumped based on how much that
variable is used, for example in conflict clauses,
and then it is exponentially decayed over time.
Overall, the combination of clever heuristics and
careful engineering allows modern SAT solvers to solve
synthesis problems with millions of variables and clauses.
Later in the course we will discuss SMT solvers, which
are built on top of SAT solvers and provide additional
expressive power that is particularly important for
dealing with verification problems.