Some formal runs finish in a second, others never finish. The difference rarely comes from the number of lines of RTL. It comes from what the logic does: wide arithmetic, deep counters, large state, and the depth of the run. This article measures run times on the lab engine for a multiplier of growing width, compares the three engines, and lists the levers you have when a run is too slow.
9 min read | Intermediate
What You'll Learn
- Name the features of a design that make formal runs slow
- Explain why wide multiplication is hard for a solver while memories and shifters are often easy
- Compare the engines of the lab on one hard design
- Choose a technique to speed up a run that times out
What Makes a Run Slow
- Depth. Every cycle adds a copy of the logic, as the unrolling article showed.
- Wide arithmetic. Multipliers, dividers and wide adder trees produce formulas that solvers find very hard, and the effort grows steeply with the width.
- Many state bits that interact. Large counters, many registers feeding each other, and long pipelines enlarge the space the solver must reason about.
- Properties that need the whole design. An end-to-end property across many modules forces the solver to consider all of them together.
Some things that look large are cheap. A memory of 256 entries checked with a tracked address took about as long as one of 4 entries on the lab engine, because the solver handles memory as an array instead of 256 separate registers. A 16-bit shift register with a property at depth 100 finished in under a second. Size in lines or bits is a poor predictor. The kind of logic matters more.
A Hard Case: Multiplication
This multiplier computes a product one bit at a time, the classic shift-and-add method, and the property compares the result with a * b:
module seqmul #(parameter W = 6) (input clk, input rst, input start, input [W-1:0] a, input [W-1:0] b,
output reg done, output reg [2*W-1:0] p);
reg [2*W-1:0] ma;
reg [W-1:0] mb;
reg [7:0] n;
reg busy;
always @(posedge clk)
if (rst) begin
busy <= 1'b0; done <= 1'b0;
end else if (start && !busy) begin
ma <= a; mb <= b; p <= 0; n <= 0; busy <= 1'b1; done <= 1'b0;
end else if (busy) begin
if (mb[0]) p <= p + ma;
ma <= ma << 1; mb <= mb >> 1; n <= n + 8'd1;
if (n == W - 1) begin busy <= 1'b0; done <= 1'b1; end
end else
done <= 1'b0;
endmodule
`ifdef FORMAL
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
// remember the operands of the job
reg [W-1:0] ra, rb;
always @(posedge clk) if (start && !busy && !rst) begin ra <= a; rb <= b; end
// when done, the product is a * b
always @(posedge clk) if (!init && done) assert (p == ra * rb);
`endif
The design is correct. Its check is a question every solver struggles with: are two different ways of multiplying the same for all inputs? The run times on the lab engine, with a depth of the width plus 4:
Each two extra bits multiply the time many times over. At 10 bits the run hits the lab limit of 300 seconds. A 32-bit multiplier checked this way would never finish. This is why datapaths with arithmetic are usually checked by other means, as the next sections show.
Quick Check
Q: The 8-bit run takes 15 seconds. Would you expect the 16-bit version to take about 30 seconds?
Show Answer
No. The growth is far steeper than linear: 10 bits already exceeds 300 seconds. A 16-bit multiplier comparison is out of reach for this approach.
The Engines
On the 8-bit multiplier, the three engines of the lab took:
| Engine | Time |
|---|---|
| Bitwuzla | 15 s |
| Boolector | 16 s |
| Z3 | 37 s |
Bitwuzla and Boolector are close, as on most bit-level designs. Z3 is often slower on such logic, as it was on the mod-60 counter in Chapter 2. The ranking can change from design to design, so trying another engine is a cheap first step when a run is slow, but no engine turns an exponential problem into an easy one.
Levers for a Slow Run
- Lower the depth to what the property needs, using the methods of the choosing a depth article.
- Shrink the design with parameters: narrower data, fewer entries, shorter counters. The 6-bit multiplier keeps the logic of the 32-bit one.
- Remove what the property does not need: black boxes and cut points replace logic that does not matter for a property with free inputs. Chapter 8 covers these.
- Split end-to-end properties into properties per module, with assume-guarantee at the boundaries.
- Treat arithmetic separately: check the control logic around a datapath formally, with the arithmetic cut out, and check the arithmetic by simulation against a reference model or with equivalence checking against a known-good version.
- Prove instead of unrolling: an induction proof needs only a few frames, so it can be faster than a deep bounded run. Chapter 6 shows how.
Quick Check
Q: A block with a 32-bit multiply-accumulate unit and a small control FSM times out. What is a sensible plan?
Show Answer
Check the FSM and its handshakes formally with the multiplier replaced by a free value (a cut point), check the multiplier itself by simulation against a reference model or by equivalence, and check a narrow version, such as 6 bits, of the full block formally.
Running It in ChipVerify Lab
The example below is the shift-and-add multiplier with W = 6, which passes in seconds. Change the parameter to 8 and run again, then try the other engines. With PRO, try 10 bits and watch the run hit the time limit.
Common Mistakes
Mistake #1: Judging run time by the size of the RTL
A few lines of multiplication can be harder than thousands of lines of control logic. Look at what the logic computes, not how long it is.
Mistake #2: Running wide arithmetic at full width
A full-width datapath check rarely finishes. Scale the width down, or cut the arithmetic out and check it separately.
Mistake #3: Waiting for a run that times out to finish next time
A run that hits the limit will not get much faster by retrying. Change something: depth, width, engine, or the scope of the property.
Practice Exercise
Challenge: Find the Limit
Open the example and run it with W = 6. Then set W = 8 and set the depth to 12, which needs PRO, and run with each of the three engines. Note the times. Finally, think about how you would verify a 32-bit version of this multiplier.
Hint
The job takes W cycles after the start, plus a few cycles for the reset, the start and the reporting step. The examples use a depth of W + 4.
Solution
On the lab engine, W = 6 passes in about a second, and W = 8 takes about 15 seconds with Bitwuzla and Boolector and about 37 with Z3. For 32 bits, check the control logic formally (start, busy, done and the cycle count) with a narrow width such as 6 bits, where the arithmetic is cheap, and check the 32-bit product by simulation against a * b with random and corner-case operands.
Open the ready-made example in the lab and run the formal check from the formal panel. The free tier allows a depth of up to 10 cycles, and PRO allows up to 100.
Summary
- Depth, wide arithmetic, interacting state and wide-scope properties make formal runs slow. Line count does not.
- A shift-and-add multiplier checked against
a * btook under a second at 6 bits, 15 seconds at 8 and over 300 seconds at 10. - Bitwuzla and Boolector were close; Z3 took more than twice as long on the same check.
- Memories and shift registers were cheap for the lab engine; multiplication was not.
- Levers: lower depth, smaller parameters, cut points, smaller properties, separate arithmetic, and induction.