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:

EngineTime
Bitwuzla15 s
Boolector16 s
Z337 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 * b took 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.
Did you find what you were looking for?

Quiz

No quiz questions available for this article.