EE219 2026 Fall — Lab3: Sigmoid LUT and Systolic GEMM

Overview

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.

Scoring breakdown

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.

References

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

https://verilator.org/guide/

Verilator is used to compile and simulate the RTL designs in this lab. Refer to the official documentation for:

Learning objectives

After Lab3, you should be able to:

Required tasks

Part A: Fixed-point Sigmoid LUT

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.

Part B: Systolic GEMM

Output-stationary systolic GEMM

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.

Transaction Interface

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
);

The transaction is defined as follows:

  1. On one rising edge, present start=1 and valid dimensions. The DUT latches M, N, and K, clears accumulators, and raises busy.
  2. Starting on a later edge, present exactly 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.
  3. The array drains its wavefront. It then raises 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.

PE and Dataflow Requirements

Each active PE is output stationary. It owns one accumulator and must:

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.

Files

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.

Run and validate

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.

Submission

Required: Report both designs:

  1. For Sigmoid, explain Q formats, signed clipping, LUT address mapping, valid-path timing, maximum/mean error, LUT storage, and multiplier/DSP count.
  2. Show the PE microarchitecture, the X/W delay scheme, one wavefront timing diagram or table, the reset and completion protocol, the result packing order, resource count for the 4 x 4 mesh, and cycle count for at least one supplied shape.
  3. Include the command output of 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