SEC: Sequential Equivalence

Prove a pipelined block equal to its reference when the flops no longer pair, with the latency written as one line of modelling code.

Lesson
F6.2
Time
20 min
Runs on
SymbiYosys
Check
5 questions

A designer pipelined an adder to meet timing, and LEC refused to pair the new flops. "Same adder, two cycles later" went into a waiver. It was true except when valid dropped for one cycle: the result already in the pipe was lost.

The result in flight when a bubble arrives
01234clkfreerstfreevalidfreeafreeb0327680gold_sum0327680pipe_validpipe_sumoperation entersbubblepipe_sum = 0, want 32768

Measured, smtbmc yices, depth 20: FAIL at reported step 4. Stage 2 waited for an input valid that never came, so it kept its old value.

Flops with No Partner

A Miter with Flops That Do Not Pairadd_comb has no flops; add_pipe2 has two stages. The bench delays the gold result by the stated latency before comparing.
A miter with flops that do not pairadd_comb has no flops; add_pipe2 has two stages. The bench delays the gold result by the stated latency before comparing. component Shared inputs valid, a, b component Gold: add_comb sum = a + b, samecycle component Gate: add_pipe2 stage 1: low bytes;stage 2: high bytes component Modelling code:gold_q1, gold_q2 {valid, sum}, one andtwo cycles back component Compare when thegate is valid pipe_sum ==gold_q2.sum
Read the diagram as text
  • Shared inputs: valid, a, b
  • Gold: add_comb: sum = a + b, same cycle
  • Gate: add_pipe2: stage 1: low bytes; stage 2: high bytes
  • Modelling code: gold_q1, gold_q2: {valid, sum}, one and two cycles back
  • Compare when the gate is valid: pipe_sum == gold_q2.sum
  • Shared inputs → Gold: add_comb
  • Shared inputs → Gate: add_pipe2
  • Gold: add_comb → Modelling code: gold_q1, gold_q2
  • Modelling code: gold_q1, gold_q2 → Compare when the gate is valid
  • Gate: add_pipe2 → Compare when the gate is valid

Nothing pairs flop to flop, so LEC has nothing to cut at. The question becomes a property over time.

Sequential equivalence drops the matched-state assumption. Gold and gate may have different flops, encodings or pipeline depths. The question is now: from reset, for every input sequence, are the outputs equal at every step, or equal after a stated latency?

That is a safety property over time, so it needs the full machinery: BMC for bugs, induction or PDR for the proof.

The Latency Is Part of the Specification

The rule in concurrent SVA.
assert property (@(posedge clk) disable iff (rst)
  pipe_valid |-> pipe_sum == $past(gold_sum, 2));
What the engine runs: the gold result kept two cycles in flops (F3.6), then compared whenever the gate says valid.
  always @(posedge clk) begin
    if ($initstate) begin assume(rst); end else begin assume(!rst); end
  end
  // modelling code: the gold's {valid, sum}, one and two cycles back
  localparam LAT = 2;
  reg [17:0] gold_q1 = 0, gold_q2 = 0;
  always @(posedge clk) begin
    gold_q1 <= rst ? 18'd0 : {gold_valid, gold_sum};
    gold_q2 <= rst ? 18'd0 : gold_q1;
  end
  wire [17:0] gold_d = LAT == 1 ? gold_q1 : gold_q2;
  always @(posedge clk) if (!rst) begin
    assert(pipe_valid == gold_d[17]);
    if (pipe_valid) assert(pipe_sum == gold_d[16:0]);
  end

The gate's output at step t equals the gold's at step t − 2. Write the latency as 1 and the proof fails at the first valid result: FAIL at reported step 3. Nothing in the design is wrong; the specification was.

Bugs, Then the Proof

RunTaskResult
Fixed pipeline, latency 2bmc, depth 20BMC PASS to depth 20
BUG_STALE_STAGE, latency 2bmc, depth 20FAIL at reported step 4
Fixed pipeline, latency 1bmc, depth 20FAIL at reported step 3
Fixed pipeline, latency 2prove, k = 2UNKNOWN at k = 2 (base case passed, induction step failed)
Fixed pipeline, latency 2prove, k = 3Prove PASS (k = 3)
Fixed pipeline, latency 2abc pdrProve PASS (abc pdr), 2.2 s
BUG_STALE_STAGEprove, k = 3UNKNOWN at k = 3 (base case passed, induction step failed)

Measured with the site's runner. The last row is a buggy design: a base case too short to reach the bug gives UNKNOWN, never PASS.

BMC found the stale stage at reported step 4. On the fixed pipeline, k-induction (F2.4) fails at k = 2 and closes at k = 3: the latency plus one. With three steps in the window, every pipeline register has been reloaded from inputs the step can see. PDR finds the same proof with no k.

Notice what the miter checks without being told: valid itself must arrive on time, so a stage that drops or doubles a result fails too. If induction ever needs help on a deeper pipeline, the helper is "stage register equals the delayed gold operand" (F4.6).

What Sequential Equivalence Covers

Change to the designCombinational LECSequential equivalence
Gate sizing, buffering, logic restructuringyesyes
Retiming across flopsno: flops moveyes
FSM re-encodingno: different state bitsyes
Added pipeline stagesno: extra flopsyes, with the latency stated
Clock gatingonly with tool supportyes, outputs equal when enabled

Commercial tools sell SEC as its own product; the bench above is the same idea on an open property checker.

EQY can also try this pair. With no flops to match it compares outputs in the same cycle, and the runner reports UNKNOWN: equivalence not proven. That is the honest result for a pipeline: EQY's partitions assume the outputs line up in time, and here they do not.

Asked in Interviews

QuestionAnswer
What breaks LEC's flop matching?Retiming, pipelining, clock gating, FSM re-encoding: any change to the state elements.
How do you set up SEC?A miter on shared inputs, the latency as modelling code, the equality gated by the gate's valid, proven with induction or PDR from reset.
What does "equivalent under a latency" mean?The same outputs for every input sequence, shifted by the stated number of cycles.

Try It

The task. The prove twin uses mode prove, depth 3.
[options]
mode bmc
depth 20
[engines]
smtbmc yices
[script]
read_verilog -formal -sv design.sv
prep -top sec_top
[files]
design.sv
  1. Find the bubble

    Run as loaded: FAIL at reported step 4. Read valid in the trace and name the cycle the operation entered.

  2. Fix the stage

    In add_pipe2, make stage 2 load on v1, the stage-1 valid. Run bmc again.

  3. Prove it

    Switch to prove. Try depth 2, then 3: UNKNOWN at k = 2 (base case passed, induction step failed), then Prove PASS (k = 3).

  4. Break the specification

    Set LAT to 1: FAIL at reported step 3.

For comparison, the EQY mode with these two files (depth 4): UNKNOWN: equivalence not proven.
module adder(input clk, rst, valid, input [15:0] a, b, output [16:0] sum, output valid_out);
  assign sum = a + b;
  assign valid_out = valid;
endmodule

module adder(input clk, rst, valid, input [15:0] a, b,
  output reg [16:0] sum = 0, output reg valid_out = 0);
  reg [8:0] lo1 = 0;            // stage 1: low bytes added, carry in bit 8
  reg [7:0] ah1 = 0, bh1 = 0;   // stage 1: high bytes, not yet added
  reg v1 = 0;
  wire [8:0] hi = ah1 + bh1 + lo1[8];
  always @(posedge clk) begin
    if (rst) begin v1 <= 0; valid_out <= 0; end
    else begin
      v1 <= valid;
      valid_out <= v1;
      if (valid) begin lo1 <= a[7:0] + b[7:0]; ah1 <= a[15:8]; bh1 <= b[15:8]; end
      if (v1) sum <= {hi, lo1[7:0]};
    end
  end
endmodule

design.sv

props.sv

Use assert property (@(posedge clk) ...);. Leave blank for immediate assertions in the design.

Inputs are free unless constrained. Run to find a counterexample or check a proof.

Model: one positive-edge clock, binary data. Sampled history requires declaration-initialized variables; primary-input history is unsupported.

SBY summary and log

Check Yourself

Predict the outputThe fixed add_pipe2 against add_comb with the miter's LAT = 1, mode bmc, default depth 20, reset assumed in step 0, smtbmc yices. The reported step is:
module add_comb(input [15:0] a, b, input valid, output [16:0] sum, output valid_out);
  assign sum = a + b;
  assign valid_out = valid;
endmodule

module add_pipe2(input clk, rst, valid, input [15:0] a, b,
  output reg [16:0] sum = 0, output reg valid_out = 0);
  reg [8:0] lo1 = 0;            // stage 1: low bytes added, carry in bit 8
  reg [7:0] ah1 = 0, bh1 = 0;   // stage 1: high bytes, not yet added
  reg v1 = 0;
  wire [8:0] hi = ah1 + bh1 + lo1[8];
  always @(posedge clk) begin
    if (rst) begin v1 <= 0; valid_out <= 0; end
    else begin
      v1 <= valid;
      valid_out <= v1;
      if (valid) begin lo1 <= a[7:0] + b[7:0]; ah1 <= a[15:8]; bh1 <= b[15:8]; end
      if (v1) sum <= {hi, lo1[7:0]};
    end
  end
endmodule

module sec_top(input clk, rst, valid, input [15:0] a, b);
  wire [16:0] gold_sum, pipe_sum;
  wire gold_valid, pipe_valid;
  add_comb  gold(.a(a), .b(b), .valid(valid), .sum(gold_sum), .valid_out(gold_valid));
  add_pipe2 pipe(.clk(clk), .rst(rst), .valid(valid), .a(a), .b(b), .sum(pipe_sum), .valid_out(pipe_valid));
`ifdef FORMAL
  always @(posedge clk) begin
    if ($initstate) begin assume(rst); end else begin assume(!rst); end
  end
  // modelling code: the gold's {valid, sum}, one and two cycles back
  localparam LAT = 1;
  reg [17:0] gold_q1 = 0, gold_q2 = 0;
  always @(posedge clk) begin
    gold_q1 <= rst ? 18'd0 : {gold_valid, gold_sum};
    gold_q2 <= rst ? 18'd0 : gold_q1;
  end
  wire [17:0] gold_d = LAT == 1 ? gold_q1 : gold_q2;
  always @(posedge clk) if (!rst) begin
    assert(pipe_valid == gold_d[17]);
    if (pipe_valid) assert(pipe_sum == gold_d[16:0]);
  end
`endif
endmodule
ConceptA reviewer adds assume(valid); to the miter "to keep the pipeline full". What happens to the stale-stage bug?
ConceptWhy does LEC fail on a pipelined rewrite, and what does SEC do instead?
Read the waveformIn the stale-stage trace, which valid pattern precedes the wrong sum?
The result in flight when a bubble arrives
01234clkfreerstfreevalidfreeafreeb0327680gold_sum0327680pipe_validpipe_sumoperation entersbubblepipe_sum = 0, want 32768

Measured, smtbmc yices, depth 20: FAIL at reported step 4. Stage 2 waited for an input valid that never came, so it kept its old value.

ConceptThe fixed pipeline proves at k = 3 but not k = 2. What sets that number?

Remember

  • Sequential equivalence lets the flops differ and asks for equal outputs over time, from reset.
  • The latency is modelling code and part of the specification; wrong by one, the proof fails.
  • BMC finds pipeline bugs; k-induction closes at the latency plus one, or PDR finds the invariant.
  • Never assume valid away: bubbles are where pipelines lose data.