Multipliers, MACs and the long path

Measure what a multiplier costs, find where the longest path of a multiply-accumulate unit really runs, and place the register that shortens it.

Lesson
R4.5
Time
30 min
Runs on
Your browser
Check
5 questions

A multiply-accumulate unit measured 39 levels of logic on this flow. Its designer blamed the 8 x 8 multiplier, which is 30 levels alone, and put a register after it. The count fell to 35, not 30. The accumulator's adder now held the longest path, and no register can go inside it.

VersionCellsFlopsLevels of logic on this flowLongest path runs
one stage: multiply and add4452139from a[2] to acc[19]
a register after the multiplier4563835from acc[1] to acc[19]
also a register inside the multiplier4746335still inside the accumulator
the 8 x 8 multiplier alone, registered3461630

Measured with Yosys 0.70 on this flow. The path ends were read from its ltp -noff report.

What a multiplier costs

A multiplier forms one partial product per bit of b: a copy of a, shifted, kept only where that bit of b is 1. Rows of adders then sum the partial products. An 8 x 8 multiplier has eight rows, and a 16 x 16 has sixteen rows of twice the width.

OperatorCellsLevels of logic on this flow
4 x 4 multiplier6412
8 x 8 multiplier33030
12 x 12 multiplier80245
16 x 16 multiplier147762
8 x 8 multiplier, both operands signed39530
8-bit adder, 8-bit sum3414
16-bit adder, 16-bit sum7730
32-bit adder, 32-bit sum17257

Measured with Yosys 0.70, each operator in a module of its own. Doubling the width multiplies the cells by about four and a half and the levels by about two.

Two things stand out. The cells grow with the square of the width, because rows and row width both double. And on this flow an adder is nearly as deep as a multiplier of the same output width: a 16-bit adder is 30 levels, the 8 x 8 multiplier 30 too. The carry has to reach the top bit either way.

Sign is the other trap. A multiply is signed only when both operands are signed; one unsigned operand makes the whole operation unsigned. Then -3 is read as 253, and the upper half of the product comes out wrong.

Where the long path of a MAC runs

A multiply-accumulate unit (MAC) adds each product into a running sum, the accumulator. Its output feeds its own adder, so the adder and the accumulator form a loop. The question is which part of the circuit sets the levels. Ask the tool: Yosys's ltp names the cells on the longest path, and the Gates tab shows its length.

The MAC, before and after the cutArrows carry data. The product register exists only in the cut version; the accumulator always feeds its own adder.
The MAC, before and after the cutArrows carry data. The product register exists only in the cut version; the accumulator always feeds its own adder. manager a, b 8 bits each bridge multiplier 30 levels alone queue prod_q 16 flops, cut only bridge adder 20 bits completer acc 20 flops operandsproductprod_qsumold acc
  1. One stage: 39 levels

    With no register in between, Yosys merges the multiply and the add into one multiply-accumulate cell. The longest path starts at a[2] and ends at acc[19]: 39 levels, fewer than 30 plus 35.

    Follow the calls, values and waits
    1. data 3 x 4: a, b → multiplier
    2. data 12: adder → acc
  2. The cut: 35 levels

    A register after the multiplier splits the path. The multiplier stage is 30 levels. The accumulator stage, from acc around through the adder and back, is 35, so 35 is the new count.

  3. The loop

    The adder reads the old accumulator and writes the new one. A register inside that loop would make the adder add an older sum, so the loop's 35 levels stay whatever you do to the multiplier.

    Follow the calls, values and waits
    1. data acc: acc → adder
Read the diagram as text
  • a, b: 8 bits each
  • multiplier: 30 levels alone
  • prod_q: 16 flops, cut only
  • adder: 20 bits
  • acc: 20 flops
  • a, b → multiplier: operands
  • multiplier → prod_q: product
  • prod_q → adder: prod_q
  • adder → acc: sum
  • acc → adder: old acc

Measured with Yosys 0.70. A further register inside the multiplier leaves the count at 35.

Why are 30 and 35 not 65? The low bits of the product are ready early, and the adder starts its carry on them while the high bits are still forming. Yosys builds the multiply and the add as one array, so the two depths overlap. Levels of a chain are measured, never added.

The cut MAC: latency 2The bench after the cut. Each pair is multiplied at the edge that takes it and added one edge later.
0123456clkin_valida3507255100b4608255100acc01242986512365223acc_valid
  1. Cycle 0

    Edge 2 takes the pair (3, 4) and stores 12 in prod_q. The accumulator has not seen it yet.

  2. Cycle 1

    Edge 3 adds 12 into acc and raises acc_valid. Two edges from taking the pair to adding it: latency 2.

  3. Cycle 2

    The gap in the input reaches the accumulator one edge later. Here acc still adds the (5, 6) pair taken at edge 3.

  4. Cycle 3

    The valid bit of the gap arrives now: acc_valid is 0 and acc holds 42. Without the valid pipe, the adder would use the wrong cycle's valid.

Measured on the lesson's bench, edges 2 to 8, with the product register added.

The lab: cut the MAC and measure

One stage

acc_valid <= in_valid;
if (in_valid)
  acc <= acc + AW'(a * b);

445 cells, 21 flops, 39 levels. Latency 1.

A register after the multiplier

prod_q     <= a * b;
prod_valid <= in_valid;
acc_valid  <= prod_valid;
if (prod_valid)
  acc <= acc + AW'(prod_q);

456 cells, 38 flops, 35 levels. Latency 2, same sums.

Design brief
PortDirectionWidthMeaning
clkinput1clock
rst_ninput1reset, asynchronous, active-low
in_validinput1a and b hold a pair this cycle
a, binput[W-1:0]the operands
accoutput[AW-1:0]the running sum: every pair with in_valid = 1 adds a x b into acc
acc_validoutput11 after the edge that adds a pair
Latency
1 as shipped; 2 after the cut, with the valid bit moving through the same stages
Parameters
W = 8, the operand width; AW = 2W + 4, the accumulator width
Corner cases
a gap in the input; 255 x 255; four bits of headroom, so up to 16 full-scale products before acc wraps

Run the starter: five pairs, none wrong, final acc 65223, latency 1. Synthesize: 39 levels. Add prod_q and prod_valid as in the right panel and run again. The sums stay the same, the latency becomes 2, and the levels drop to 35. Measured: forget the valid pipe and four of the five sums are wrong.

Then set W to 16 and Synthesize both versions: 1696 cells and 69 levels in one stage, 1734 cells and 64 levels with the cut. The wider accumulator's loop again sets the count.

Asked in interviews
  1. How do you speed up a MAC?

    Put a register between the multiplier and the accumulator and move the valid bit with it. That takes the multiply out of the accumulator's cycle. After that the accumulator's adder, in its loop, is the floor; measured here, 39 levels became 35.

  2. What does a multiplier cost?

    Cells grow with the square of the width and depth with the width. Measured on this flow: 330 cells and 30 levels at 8 x 8, 1477 and 62 at 16 x 16. A constant operand or a smaller width is the cheap fix.

  3. Where is the critical path in a datapath, and how do you find it?

    Ask the tool. A static timing report names the slowest path in a real flow; here Yosys's ltp lists the cells on the longest path. It often ends somewhere unexpected, such as an accumulator rather than the multiplier.

Try it

Each pair is multiplied and added at the edge that takes it: 12 at edge 2, 12 + 30 = 42 at edge 3, and 65123 after 255 x 255. The gap at edge 4 leaves the sum alone.

With the product register the same lines appear one edge later, the last line reads latency 2, and Synthesize reports 35 levels instead of 39.

RTLIcarus Verilog · in your browser · Lint: Verilator service · Synthesize: Yosys service
Edit the design, then Run, Lint or Synthesize
Expected: e0 in_valid=0 a=0 b=0 acc=0 acc_valid=0 e1 in_valid=0 a=0 b=0 acc=0 acc_valid=0 e2 in_valid=1 a=3 b=4 acc=12 acc_valid=1 e3 in_valid=1 a=5 b=6 acc=42 acc_valid=1 e4 in_valid=0 a=0 b=0 acc=42 acc_valid=0 e5 in_valid=1 a=7 b=8 acc=98 acc_valid=1 e6 in_valid=1 a=255 b=255 acc=65123 acc_valid=1 e7 in_valid=1 a=10 b=10 acc=65223 acc_valid=1 e8 in_valid=0 a=0 b=0 acc=65223 acc_valid=0 e9 in_valid=0 a=0 b=0 acc=65223 acc_valid=0 e10 in_valid=0 a=0 b=0 acc=65223 acc_valid=0 5 pairs, 0 wrong, final acc=65223, latency 1

Original design · 445 cells · 21 flip-flops · 0 latches · 39 logic levels. Synthesis uses Yosys; counts depend on the toolchain.

Read included file: tb.sv
`timescale 1ns/1ps
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                   // rising edges at 5, 15, 25, ...
  logic rst_n = 1;                        // falls at 1 ns: a real edge, so an asynchronous reset acts before any clock
  int edge_n = -1;
  always @(posedge clk) edge_n = edge_n + 1;
  logic        in_valid = 0;
  logic [7:0]  a = 0, b = 0;
  logic [19:0] acc;
  logic        acc_valid;
  logic [19:0] sum = 0;                   // the model: the running sum, 20 bits like acc
  logic [19:0] want[$];                   // the sum the design should show for each pair taken
  int          took[$];                   // the edge that took each pair
  int pairs = 0, wrong = 0, latency = 0;
  task automatic send(input logic [7:0] x, input logic [7:0] y);
    in_valid = 1; a = x; b = y; tick();
  endtask
  task automatic idle();
    in_valid = 0; a = 0; b = 0; tick();
  endtask
  mac dut (.clk(clk), .rst_n(rst_n), .in_valid(in_valid), .a(a), .b(b), .acc(acc), .acc_valid(acc_valid));
  // Advance to the next rising edge, wait for the design to settle, then print one line.
  task automatic tick();
    @(posedge clk); #1;
    if (in_valid) begin sum = sum + a * b; want.push_back(sum); took.push_back(edge_n); end
    if (acc_valid) begin
      latency = edge_n - took.pop_front() + 1;
      if (acc !== want.pop_front()) wrong++;
      pairs++;
    end
    $display("e%0d in_valid=%b a=%0d b=%0d acc=%0d acc_valid=%b", edge_n, in_valid, a, b, acc, acc_valid);
  endtask
  initial begin
    #1 rst_n = 0; tick(); tick(); rst_n = 1;   // asserted at 1 ns, edges 0 and 1 in reset, released 1 ns after edge 1
    send(3, 4); send(5, 6); idle(); send(7, 8); send(255, 255); send(10, 10);
    idle(); idle(); idle();
    $display("%0d pairs, %0d wrong, final acc=%0d, latency %0d", pairs, wrong, acc, latency);
    $finish(0);                             // quiet: no "$finish called" line
  end
endmodule

Check yourself

Predict the outputThe cut MAC (latency 2) takes three pairs on three edges in a row: (2, 3), (4, 5), (6, 7). The program prints acc right after the third edge. What is the output line?
// mac: a multiply-accumulate unit. Each valid pair (a, b) adds a * b into acc.
module mac #(
  parameter int W  = 8,
  parameter int AW = 2*W + 4
) (
  input  logic          clk,
  input  logic          rst_n,
  input  logic          in_valid,
  input  logic [W-1:0]  a, b,
  output logic [AW-1:0] acc,
  output logic          acc_valid
);
  logic [2*W-1:0] prod_q;                // stage 1: the product, registered
  logic           prod_valid;            // ... and the valid bit that travels with it
  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) begin
      prod_q     <= '0;
      prod_valid <= 1'b0;
      acc        <= '0;
      acc_valid  <= 1'b0;
    end else begin
      prod_q     <= a * b;                 // stage 1: multiply
      prod_valid <= in_valid;
      acc_valid  <= prod_valid;            // stage 2: accumulate
      if (prod_valid) acc <= acc + AW'(prod_q);
    end
endmodule
module tb;
  logic clk = 0, rst_n = 1, in_valid = 0;
  logic [7:0] a = 0, b = 0;
  logic [19:0] acc;
  logic acc_valid;
  mac dut (.clk(clk), .rst_n(rst_n), .in_valid(in_valid), .a(a), .b(b), .acc(acc), .acc_valid(acc_valid));
  task automatic step(); #1 clk = 1; #1 clk = 0; endtask
  initial begin
    #1 rst_n = 0;                 // reset pulse: acc = 0
    #1 rst_n = 1;
    in_valid = 1;
    a = 2; b = 3; step();     // edge 1 captures (2, 3)
    a = 4; b = 5; step();     // edge 2 captures (4, 5)
    a = 6; b = 7; step();     // edge 3 captures (6, 7)
    $display("acc=%0d", acc);
    $finish(0);
  end
endmodule
Spot the bugEach option is the body of a module that multiplies two 8-bit two's-complement numbers into a 16-bit product. The ports are unsigned logic. The bench tries -3 x 5, -4 x -4, 100 x -2 and 7 x 9. Which option fails?
ConceptAfter the register went in after the multiplier, the MAC measured 35 levels, not the multiplier's 30. Why?
ConceptA team moves from 8 x 8 to 16 x 16 operands. What does this flow measure for the multiplier alone?
Read the waveformThis is the cut MAC on the lab bench. The pair (5, 6) is taken in cycle 1. In which cycle does acc first include 5 x 6?
The cut MAC on the lab bench
01234in_valida3507255b4608255acc0124298acc_valid

One value per edge, from edge 2 of the bench, printed after the edge settles.

Remember

  • A multiplier's cells grow with the square of its width and its levels with the width: 330 cells and 30 levels at 8 x 8, 1477 and 62 at 16 x 16, on this flow.
  • A multiply is signed only when both operands are signed; one unsigned operand makes the whole operation unsigned.
  • Find the long path by asking the tool, never by adding operator depths: the one-stage MAC is 39 levels, not 30 plus 35.
  • A register between the multiplier and the accumulator takes the multiply out of the loop; the valid bit moves through the same stages, and the latency grows by one.
  • The accumulator's adder sits in a loop, so it sets a floor that more multiplier stages cannot lower: 35 levels here.