Lab3 contains two independent RTL exercises: a fixed-point Sigmoid lookup
table (Part A), and an unsigned 4 x 4 output-stationary systolic array for
GEMM (Part B). Complete vsrc/src/sigmoid.v, vsrc/src/pe.v, and
vsrc/src/systolic_array.v without changing their paths, module names,
parameters, or port lists.
The Sigmoid input is signed Q5.11; GEMM operands remain unsigned UINT8 and its accumulators remain unsigned UINT32. The two parts are tested separately; no accumulator-to-Sigmoid conversion or combined pipeline is required.
| Deliverable | Weight |
|---|---|
| Sigmoid LUT, PE, and systolic array RTL | 65% |
| Report: fixed point, approximation error, microarchitecture, wavefront timing, protocol | 20% |
| Verification output and boundary-case handling | 15% |
The existing RTL/report/verification weights apply to both parts together.
The Lab3 environment guide is an additional reference for SISI students. SIST students who prefer not to use the cloud environment can also follow it to set up a personal computer or Linux machine.
The following documentation is used in this lab. You are encouraged to read it before implementing the TODOs.
Verilator User's Guide
Verilator is used to compile and simulate the RTL designs in this lab. Refer to the official documentation for:
After Lab3, you should be able to:
UINT8 × UINT8 → UINT32 multiply-accumulate arithmetic;Implement sigmoid(x) = 1 / (1 + exp(-x)) using a 64-entry uniform lookup
table without interpolation or runtime floating-point arithmetic. The complete
64-entry table is provided in the starter as the function lut_value(index).
Keep its constants unchanged; students implement all other logic.
| Item | Required format |
|---|---|
| Input | Signed 16-bit Q5.11, decoded as x_q5_11 / 2048 |
| Representable input range | [-16, 16 - 1/2048] |
| Clipping range | [-8, 8] |
| Output | Unsigned 16-bit Q0.15 code, scaled by 32767 |
| Table | 64 entries of 16 bits; uniform bins of width 0.25 |
| Sample point | Centre of each bin |
| Rounding | Provided LUT entries are already rounded to nearest |
| Latency | One rising edge after the input is accepted |
| Throughput | One input per cycle |
The output convention is the historical Lab3 convention: encode y as
round(y * 32767) and decode it as y_q0_15 / 32767.
Implement vsrc/src/sigmoid.v with the provided interface and LUT function:
module sigmoid (
input wire clk,
input wire rst_n,
input wire in_valid,
input wire signed [15:0] x_q5_11,
output reg out_valid,
output reg [15:0] y_q0_15
);
An input is accepted at a rising edge when in_valid=1. Assert out_valid
with that input's result at the next rising edge. Accept consecutive inputs
at one per cycle. When rst_n=0, clear all pending valid state and drive
out_valid=0, discarding any input pending before reset. The value of
y_q0_15 is ignored when out_valid=0.
Use this exact mapping:
clipped_code = max(-16384, min(16384, signed_input_code))
index = min((clipped_code + 16384) // 512, 63)
LUT[i] = round(32767 * sigmoid(-8 + (i + 0.5) * 0.25))
y_q0_15 = LUT[index]
Sign-extend the input before clipping. Use at least 17 bits for the offset
addition: +8 maps to code 16384, and adding 16384 produces 32768.
Clamp the address at 63 so the upper clipping endpoint cannot address entry
64. After calculating the 6-bit address, call the provided lut_value(index)
function. No table generation is required. Students must implement signed
clipping, address calculation, all pipeline registers, in_valid/out_valid
timing, and reset flushing; only the interface and LUT are supplied.
The checker requires the specified LUT codes exactly and also reports error
against a clipped FP32 Sigmoid reference. Maximum absolute error must be no
larger than 0.035; mean absolute error must be no larger than 0.012.
The deterministic input set contains 1024 values, including all 64 bins,
bin boundaries and centres, clipping endpoints, signed extrema, zero, and
random inputs. A 1031-cycle schedule also checks consecutive valid inputs,
an invalid gap, reset flushing, and exact one-cycle latency.
The array calculates the unsigned integer GEMM below:
Y[M, K] = X[M, N] * W[N, K]
Y[m, k] = sum(X[m, n] * W[n, k]) for n in 0 .. N-1
The supplied array has a physical 4 x 4 PE mesh. Each individual test may
use 1 <= M, N, K <= 4; PEs outside the requested M x K result rectangle
must be bypassed. Inputs and weights are unsigned 8-bit values, and each
output accumulator is unsigned 32-bit. The supplied vectors include 0,
15, 127, 128, 200, and 255 to check unsigned arithmetic across the
signed 8-bit boundary; the scalar 200 * 3 must produce 600.
The arithmetic is UINT8 × UINT8 → UINT32; products must be zero-extended
before accumulation.
Implement vsrc/src/pe.v and vsrc/src/systolic_array.v without changing
their module names or port lists. The top-level input/output format is:
module systolic_array #(
parameter M_MAX = 4,
parameter N_MAX = 4,
parameter K_MAX = 4,
parameter DATA_WIDTH = 8,
parameter ACC_WIDTH = 32
) (
input wire clk,
input wire rst_n,
input wire start,
input wire [2:0] m_size,
input wire [2:0] n_size,
input wire [2:0] k_size,
input wire in_valid,
input wire [DATA_WIDTH*M_MAX-1:0] x_vector,
input wire [DATA_WIDTH*K_MAX-1:0] w_vector,
output reg busy,
output reg out_valid,
output reg done,
output reg [ACC_WIDTH*M_MAX*K_MAX-1:0] y_matrix
);
x_vector[DATA_WIDTH*r +: DATA_WIDTH] is the input for matrix row r,w_vector[DATA_WIDTH*k +: DATA_WIDTH] is the input for output column k.y_matrix[ACC_WIDTH*(r*K_MAX+k) +: ACC_WIDTH] is Y[r][k].The transaction is defined as follows:
start=1 and valid dimensions. The DUT latches
M, N, and K, clears accumulators, and raises busy.N consecutive in_valid=1
beats. On beat n, the valid lanes contain X[0:M-1, n] and W[n, 0:K-1].
Unused lanes are ignored; the supplied testbench drives them with nonzero
values so that a missing bypass is observable.out_valid and done
together for one cycle, with the complete result in y_matrix, and lowers
busy. No new transaction starts until this pulse has completed.The environment has no input-ready signal and does not insert gaps. Supporting
gaps is optional; when in_valid=0, an implementation must not count a new
inner-dimension beat.
Each active PE is output stationary. It owns one accumulator and must:
x_in * w_in to its
local accumulator.start edge (the array passes
this internal control to each PE).bypass=1). Inactive
y_matrix lanes must remain zero after the start clear; the checker uses
those lanes to verify bypass.For the standalone PE, clear_acc=1 clears both operand forwarding registers,
both outgoing valid bits, and the accumulator on the rising edge, just as
reset does. Reset and clear take priority over bypass and valid inputs.
Otherwise, X, W, and their independent valid bits are forwarded with one
cycle of latency, including when bypass is set or an input valid bit is low.
The accumulator holds unless both valid bits are high and bypass=0.
To align operands at PE (r, k), delay the X input for row r by r cycles
before its first PE and delay the W input for column k by k cycles before
its first PE. The X and W valid bits must receive identical delays. From there,
each PE's registered forwarding adds the remaining horizontal or vertical
delay. The final active PE receives its last pair M + K - 2 cycles after the
final input beat; capture the result on the following edge.
For example, a 2 x 3 by 3 x 2 multiply feeds three beats:
| Beat | x_vector lanes |
w_vector lanes |
|---|---|---|
| 0 | X[0,0], X[1,0] |
W[0,0], W[0,1] |
| 1 | X[0,1], X[1,1] |
W[1,0], W[1,1] |
| 2 | X[0,2], X[1,2] |
W[2,0], W[2,1] |
The testbench checks six matrix shapes (1,1,1), (1,4,3), (4,1,2),
(2,3,4), (4,4,4), and (3,2,1), plus four scalar unsigned-boundary
cases. They exercise scalar operation, rectangular matrices, all four edges
of the physical mesh, and drain timing. The same testbench also checks
busy, the one-cycle out_valid/done pulse, exact drain timing, reset
flush, and zero inactive lanes.
vsrc/src/sigmoid.v Sigmoid interface and complete 64-entry LUT
vsrc/src/pe.v PE interface-only starter
vsrc/src/systolic_array.v Array interface-only starter
vsrc/sim/tb_sigmoid.sv Sigmoid arithmetic and cycle-level testbench
vsrc/sim/tb_pe.sv Standalone PE timing and arithmetic checks
vsrc/sim/tb_systolic_array.sv Streaming GEMM and protocol testbench
scripts/generate_vectors.py Deterministic Sigmoid vectors and schedule
scripts/test.py Sigmoid LUT, FP32 error, and timing checker
scripts/generate_systolic_vectors.py Deterministic GEMM vector generator
scripts/check_systolic.py Exact-result checker
The PE and array starters provide only parameters, ports, and TODO comments.
Implement their internal registers, datapath, and control yourself. The
Sigmoid starter supplies the interface and LUT values only; implement its
clipping, address calculation, pipeline, valid handling, and reset yourself.
make test is expected to fail before the TODOs are completed.
After completing the TODOs, run:
SISI environment (Ubuntu 24.04 LTS in WSL2):
make sigmoid # verify Sigmoid LUT, approximation error, and timing
make pe # independently verify the PE first
make systolic # verify GEMM and the array protocol
make test # required: Sigmoid, PE, and array checks
SIST environment (VMWare Horizon, "torch" envorinment):
make sigmoid CXX="$CONDA_PREFIX/bin/x86_64-conda-linux-gnu-g++"
make pe CXX="$CONDA_PREFIX/bin/x86_64-conda-linux-gnu-g++"
make systolic CXX="$CONDA_PREFIX/bin/x86_64-conda-linux-gnu-g++"
make test CXX="$CONDA_PREFIX/bin/x86_64-conda-linux-gnu-g++"
The PE test checks registered forwarding, all four input-valid combinations, unsigned boundary products, consecutive accumulation, bypass, reset, clear, and recovery after reset/clear. Results use exact integer comparisons with no tolerance. Numerical and protocol mismatches are collected while the remaining cases continue to run. At the end, failed PE/GEMM case numbers and Sigmoid cycle numbers are listed without numerical details.
make test runs all three parts even if an earlier part fails, then reports
the failed cases and returns a nonzero exit status. Individual build and test
logs are saved in build/sigmoid_test.log, build/pe_test.log, and
build/systolic_test.log.
For waveform debugging, enable Verilator tracing with --trace and define
ENABLE_TRACE to activate the testbench's VCD output. The Makefile supports
this with make sigmoid TRACE=1 (or make pe TRACE=1 / make systolic TRACE=1).
You can also use Icarus
Verilog (iverilog) to simulate the design and GTKWave to inspect VCD waveforms.
Required: Report both designs:
4 x 4 mesh, and cycle count
for at least one supplied shape.make test after completion.Required: Package sigmoid.v, pe.v, systolic_array.v, and the report
together in a ZIP or TAR archive, then submit the archive. Do not include
build artifacts or generated vectors/results.
Required: If you used AI, follow the class requirement and submit the separate AI report on Gradescope