Implementing the Accelerator
Rodolfo Azevedo
Institute of Computing, University of Campinas (UNICAMP), Brazil
rodolfo.azevedo@unicamp.br
http://www.ic.unicamp.br/~rodolfo/mo801
Goal of this class
Module 5, Class 2: the int8 MAC array, local BRAM buffer, accumulator, and control FSM in SystemVerilog.
This is the implementation class. The spec is fixed (M04A04). The goal is a Verilator testbench that passes for all test vectors before we connect anything to the bus.
At the end of this class, you should be able to:
- Implement an int8 MAC array in SystemVerilog and verify it produces correct results with Verilator.
- Design the local BRAM buffer and accumulator for the KWS layer computation.
- Write the control FSM that sequences data fetch, multiply-accumulate, and writeback operations.
- Run the accelerator testbench against all test vectors to confirm bit-identical results before bus integration.
Top-level module interface
The accelerator has two interfaces: * Bus interface — CPU configures registers and reads STATUS. * DMEM interface — accelerator fetches input activations directly from data memory (DMA-like). Weights are pre-loaded into the local BRAM by the CPU via the bus.
The int8 MAC unit
A single int8 MAC: multiply two 8-bit signed integers and accumulate into a 32-bit register.
a * b— the synthesis tool infers a DSP block for this.- Sign-extension:
productis 16-bit signed; extending to 32 bits before accumulation prevents overflow for up to $2^{16}$ accumulations at max value (127 × 127 = 16,129).
4-wide MAC array
Four MAC units operating in parallel on four lanes:
Each lane accumulates one partial result. After LENGTH/LANES cycles, each accumulator holds a partial dot product. Sum the four accumulators to get the final result.
For the Conv2D inner loop (length = KH×KW×C_in, 4 lanes): length/4 MAC cycles to compute one output value, vs. length cycles for a scalar implementation.
Local weight BRAM buffer
The weight buffer holds one row of the weight matrix (64 int8 values = 64 bytes):
The read port delivers 4 int8 values in one cycle — matched to the 4-wide MAC array. One full weight row (64 int8) takes 16 read cycles to deliver to all 4 lanes.
Requantization in hardware
The int32 accumulator must be requantized to int8 before writing the output:
The multiplier+shift trick avoids floating-point: scale ≈ multiplier × 2^{-shift}, precomputed by the training framework and stored in weights.h as integer constants.
Control FSM: IDLE → LOAD → COMPUTE → DONE
LOAD and COMPUTE can be overlapped (load next activation while current MACs are computing) for a 2× throughput gain — a pipeline optimization for later.
Timing analysis: cycles per inference call
For one Conv2D output value (one output channel, one spatial position), with LENGTH = KH×KW×C_in, LANES = 4:
| Phase | Duration | Notes |
|---|---|---|
| LOAD (activations from DMEM) | LENGTH/4 cycles |
4 int8 values fetched per cycle; may stall if DMEM busy |
| COMPUTE (MAC array) | LENGTH/4 cycles |
Runs in lock-step with weight BRAM reads |
| WRITEBACK (requantize + write) | 1–2 cycles | Combinational requant + one DMEM write |
| Total per output value | LENGTH/2 + 2 cycles |
(without LOAD/COMPUTE overlap) |
For the full Conv2D layer with H_out × W_out × C_out output values:
$$\text{Total cycles} \approx H_{out} \times W_{out} \times C_{out} \times \frac{K_H \times K_W \times C_{in}}{2}$$
Compare to the software baseline:
$$\text{SW cycles} \approx H_{out} \times W_{out} \times C_{out} \times K_H \times K_W \times C_{in} \times 4 \text{ (cycles/MAC)}$$
Predicted speedup = SW / HW ≈ $4 \times K_H \times K_W \times C_{in} / (K_H \times K_W \times C_{in}/2)$ = 8× — but Amdahl's Law reduces system speedup to ~4.5× for $f = 0.92$.
This predicted speedup assumes no stalls and no bus overhead. Your measured number will be lower. The gap between predicted and measured is where debugging happens.
Verification: directed test against the C kernel
Run with make sim. Only after all assertions pass: connect to the bus.
In-class exercise — FSM trace
Trace the control FSM for a length-8 dot product (LENGTH = 8, LANES = 4):
Fill in the table cycle by cycle (start from IDLE, then start asserts):
| Cycle | State | dmem_addr |
mac_en |
mac_clear |
done |
Notes |
|---|---|---|---|---|---|---|
| 0 | IDLE | — | 0 | 0 | 0 | waiting |
| 1 | LOAD | start asserted | ||||
| 2 | LOAD | |||||
| 3 | COMPUTE | length/4=2 load cycles done | ||||
| 4 | COMPUTE | |||||
| 5 | DONE | |||||
| 6 | IDLE | STATUS read by CPU |
Questions:
1. In cycle 1, what address does the accelerator put on dmem_addr? (Hint: INPUT_ADDR register value)
2. In cycle 3, what happens to the weight BRAM address? Does it reset or continue from where LOAD left off?
3. If dmem_ready is deasserted for one cycle during LOAD, how does the total cycle count change?
Expected: 2 LOAD cycles (8 elements / 4 lanes), 2 COMPUTE cycles, 1 DONE cycle = 5 active cycles + overhead. A
dmem_readystall in LOAD extends that phase by 1 cycle.
Pipelining LOAD and COMPUTE
The basic FSM does LOAD then COMPUTE sequentially. With double-buffering, they can overlap:
Ideal throughput with overlap:
* Without overlap: 2 × (LENGTH/4) cycles per output value.
* With overlap: LENGTH/4 + startup cycles — the LOAD and COMPUTE phases run in parallel.
* For LENGTH = 64: 16 cycles (overlapped) vs. 32 cycles (sequential) — 2× throughput improvement.
Implementation cost: one extra BRAM for the second activation buffer, and a more complex FSM. This is the "pipeline optimization for later" mentioned in the base FSM slide.
This is the same principle as the multicycle vs. pipelined processor from M02: separate "stages" that can run simultaneously. The accelerator FSM is a 2-stage pipeline: fetch activations (stage 1) and compute MACs (stage 2).
Next class
Integration & End-to-End Measurement: wiring the accelerator as a memory-mapped peripheral, the C software driver, replacing the software PW conv with accel_run(), and measuring cycle counts before and after.