Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ scripts/*.onnx
scripts/*.png
.DS_Store
__cmake_systeminformation
synth_outputs
31 changes: 31 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ make -j
Key CMake flags:
- `-DSIM=ON` — enables Verilator backend (required for end-to-end runs)
- `-DSIM_ROWS=N -DSIM_COLS=N` — override array size (default **64×64**)
- `$SKYWATER_LIB` — **env var** pointing to the SkyWater130 Liberty .lib file (required for `synth` target)
- `-DCLOCK_PERIOD_NS=N` — clock period in ns for ABC timing-driven mapping (default: **10**)

The EP shared library is built at `build/onnx-plugin/libtinyxpu_ep.{so,dylib}`.

Expand Down Expand Up @@ -56,6 +58,35 @@ python scripts/run_matmul.py # ONNX → TinyXPU EP → verify vs NumPy

`run_matmul.py` defaults to `scripts/matmul_integer_16x16.onnx` and the plugin at `build/onnx-plugin/libtinyxpu_ep.{so,dylib}`. It will fail with a helpful message if either is missing.

## Frequency Analysis

```sh
export SKYWATER_LIB=/path/to/sky130_fd_sc_hd__tt_025C_1v80.lib # see README
cmake -B build -DSIM=ON -DCLOCK_PERIOD_NS=10
cmake --build build --target synth
```

### Clock Period Sweep

Sweep `CLOCK_PERIOD_NS` from 2 to 20 ns to see the area vs frequency tradeoff:

```sh
./scripts/sweep_period.sh
```

This runs `cmake` + `make synth` for each period, saving per-period results to `synth_outputs/<period>ns/` (stats, timing, netlist) and printing a summary table of cells, gate area, and delay.

**Memory note:** The sweep uses `SYNTH_COLS=1` (single column) because ABC's
timing-driven mapping (`&fraig`, `&dch -f`) blows past 10 GB on a 16×16 array
at 2 ns. The critical timing path is the accumulator chain down one column
— all columns have identical delay, so single-column timing is representative.
Multiply the reported gate area ×16 for an estimated full-array area. To
synthesise the full array, pass `-DSYNTH_COLS=16` to cmake (needs >10 GB RAM).

The `synth` target synthesises the array to SkyWater130 cells with ABC timing-driven mapping.
ABC's `stime -p` reports the estimated critical path delay.
Outputs go to `synth_outputs/`: `synth.json` (netlist), `synth_stats.txt` (cells), `synth_timing.rpt` (timing).

## Architecture Notes

- **PE (`pe.sv`):** `weight_ld=1` latches `acc_in` as a new weight, sets `acc_out` to the weight value (cascading it south), and sets `data_out=0`. `weight_ld=0` performs MAC: `acc_out = acc_in + weight_r * data_in` (first untagged input is bias, subsequent are partial sums). `int8×int8→int32` MAC.
Expand Down
22 changes: 2 additions & 20 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,8 @@ add_custom_command(

add_custom_target(compile ALL DEPENDS ${COMPILED_VVP})

# ---------- optional: Yosys synthesis ----------
if(YOSYS)
set(SYNTH_JSON "${CMAKE_BINARY_DIR}/synth.json")

# Build a read_verilog command for each source file
set(YOSYS_READ_CMDS "")
foreach(src ${SV_SOURCES})
string(APPEND YOSYS_READ_CMDS "read_verilog -sv ${src}; ")
endforeach()

add_custom_command(
OUTPUT ${SYNTH_JSON}
COMMAND ${YOSYS} -p
"${YOSYS_READ_CMDS} synth -top pe; write_json ${SYNTH_JSON}"
DEPENDS ${SV_SOURCES}
COMMENT "Synthesising with Yosys"
)

add_custom_target(synth DEPENDS ${SYNTH_JSON})
endif()
# ---------- optional: Yosys synthesis (cmake/synth.cmake) ----------
include(cmake/synth.cmake)

add_subdirectory(onnx-plugin)

Expand Down
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ ctest --verbose # waveforms written to test/sim_build/*.fst
Key CMake flags:
- `-DSIM=ON` — use Verilator simulation backend (required for software runs)
- `-DSIM_ROWS=N -DSIM_COLS=N` — override array size (default **64×64**)
- `$SKYWATER_LIB` — **env var** pointing to the SkyWater130 Liberty .lib file (required for `synth` target)
- `-DCLOCK_PERIOD_NS=N` — clock period in ns for ABC timing-driven mapping (default: **10**)

Install the [Surfer](https://marketplace.visualstudio.com/items?itemName=surfer-project.surfer) VSCode extension to view `.fst` waveforms.

Expand All @@ -67,6 +69,54 @@ python scripts/run_matmul.py # 2-D MatMulInteger via Verilator, verifies vs
python scripts/test_ops.py # batched MatMulInteger + Gemm tests
```

## Frequency Analysis

Synthesise the array to SkyWater130 standard cells and run static timing analysis to estimate the maximum clock frequency.

### 1. Download the SkyWater130 PDK Library

```sh
brew install zstd
cd ~/projects # where you want to put the PDK
curl -L -o sky130_fd_sc_hd.tar.zst \
https://github.com/fossi-foundation/ciel-releases/releases/download/sky130-ff08c23db8359afce3f134c454e7930586d0641c/sky130_fd_sc_hd.tar.zst
tar --zstd -xf sky130_fd_sc_hd.tar.zst
ls "$PWD/sky130A/libs.ref/sky130_fd_sc_hd/lib/sky130_fd_sc_hd__tt_025C_1v80.lib"
```
Copy the path, and export it as `SKYWATER_LIB` in your shell rc file.

Add the `export` line to your shell rc file for persistence.

### 2. Build with Synthesis & STA

```sh
mkdir -p build && cd build
cmake .. -DSIM=ON -DCLOCK_PERIOD_NS=10
make synth
```

The `synth` target runs Yosys to:
1. Read the RTL
2. Elaborate and flatten the array
3. Map DFFs and combinational logic to SkyWater130 cells via ABC (timing-driven with `-D CLOCK_PERIOD_NS`)
4. Report ABC's estimated critical path delay, area, and cell counts

**Output files (in `synth_outputs/`):**
- `synth.json` — gate-level netlist
- `synth_stats.txt` — cell counts
- `synth_timing.rpt` — ABC timing log (critical path delay)

**Key line in `synth_outputs/synth_timing.rpt`:**
```
ABC: Gates = 129761 Area = 928743.25 Delay = 10676.35 ps
```
This is ABC's pre-layout critical path estimate (no wireload model). The
reported delay is optimistic vs. post-P&R but gives a useful frequency target.

**Note:** Yosys's built-in `sta` command is unavailable due to a
[known bug](https://github.com/YosysHQ/yosys/issues/4232) with non-parametric
blackbox cells. ABC's `stime -p` provides a comparable pre-layout estimate.

## Systolic Array Architecture

A `ROWS × COLS` PE grid. Dataflow is **weight-stationary**: weights load once, then activations stream east (→) while partial sums cascade south (↓).
Expand Down
205 changes: 205 additions & 0 deletions cmake/synth.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# Yosys synthesis target for the systolic array.
#
# Yosys does not support unpacked array module ports (used in array.sv and
# pe_col.sv). This file generates a flat module that directly instantiates
# every PE with individual scalar wires — no unpacked arrays anywhere.
#
# Exposes a `synth` target that runs:
# read_verilog → hierarchy → flatten → proc → techmap →
# dfflibmap → abc -liberty (stime -p timing report) → opt_clean →
# write_json → stat
#
# Requires $SKYWATER_LIB env var pointing to the SkyWater130 Liberty .lib file.
# Timing estimates, cell statistics, and gate-level netlist are written to
# <project>/synth_outputs/.
#
# Memory note: synth flattens all PEs into one module then passes the entire
# netlist to ABC. A 16×16 array (256 PEs) under tight timing can exceed 10 GB
# RAM because ABC's &fraig and &dch -f commands blow up on large flat designs.
#
# The critical timing path is the accumulator chain down a single column
# (ROWS PEs deep). All columns have identical delay. So we default to
# SYNTH_COLS=1 — one column, same timing, a fraction of the memory.
# Set SYNTH_COLS=16 to synthesise the full array (needs >10 GB for 2 ns).

if(NOT YOSYS)
return()
endif()

# --- SkyWater130 Liberty library (env var only, no caching) ---
if(NOT DEFINED ENV{SKYWATER_LIB} OR "$ENV{SKYWATER_LIB}" STREQUAL "")
message(FATAL_ERROR
"Set the SKYWATER_LIB env var to the path of sky130_fd_sc_hd__tt_025C_1v80.lib.\n"
"See README.md for download instructions.")
endif()
if(NOT EXISTS "$ENV{SKYWATER_LIB}")
message(FATAL_ERROR "SKYWATER_LIB=$ENV{SKYWATER_LIB} does not exist")
endif()

# --- Clock period for timing-driven mapping ---
set(CLOCK_PERIOD_NS 10 CACHE STRING "Clock period in ns for ABC timing-driven mapping")
math(EXPR CLOCK_PERIOD_PS "${CLOCK_PERIOD_NS} * 1000")
# Stamp file so make rebuilds when the clock period changes (the Yosys command
# embeds CLOCK_PERIOD_PS in the -D flag but make can't see command changes).
set(CLOCK_PERIOD_STAMP "${CMAKE_BINARY_DIR}/clock_period.stamp")
file(WRITE ${CLOCK_PERIOD_STAMP} "${CLOCK_PERIOD_PS}")

# --- Output directory for synthesis results ---
set(SYNTH_OUT "${CMAKE_SOURCE_DIR}/synth_outputs")
file(MAKE_DIRECTORY ${SYNTH_OUT})

# --- ABC timing script (generated at configure time) ---
# The script first runs technology-independent optimisation (fraig, scorr,
# dc2, dretime), then delay-constrained technology mapping (map) followed
# by area-recovery choice (dch). The -D flag on yosys's abc pass sets a
# global delay target that map and dch both respect — tighter periods
# select higher-drive cells, relaxed periods select smaller cells.
# The clock period is written into the file as a comment so Make detects
# period changes as a content change and triggers a rebuild.
set(ABC_SCRIPT "${CMAKE_BINARY_DIR}/abc_timing.script")
file(WRITE ${ABC_SCRIPT}
"strash; &get -n; &fraig -x; &put; scorr; dc2; dretime; strash; map -D {D}; stime -p\n")

set(SYNTH_JSON "${SYNTH_OUT}/synth.json")

# Cache variables for array dimensions (shared with onnx-plugin).
set(SIM_ROWS 16 CACHE STRING "Systolic array row count (Verilator + sim)")
set(SIM_COLS 16 CACHE STRING "Systolic array column count (Verilator + sim)")

# Synthesis array dimensions — decoupled from SIM_* so we can use a single
# column for low-memory synthesis while keeping the full array for simulation.
# The critical timing path is the accumulator chain down a column (SYNTH_ROWS
# PEs deep). A single column gives the same delay as the full array.
set(SYNTH_ROWS "${SIM_ROWS}" CACHE STRING "Systolic array rows for synthesis")
set(SYNTH_COLS 1 CACHE STRING "Systolic array columns for synthesis")

set(SYNTH_SRC "${CMAKE_BINARY_DIR}/synth_array.sv")
set(DW 8)
set(AW 32)
math(EXPR MAX_R "${SYNTH_ROWS} - 1")
math(EXPR MAX_C "${SYNTH_COLS} - 1")

file(WRITE ${SYNTH_SRC}
"// Auto-generated flat array for Yosys synthesis\n"
"// Array: ${SYNTH_ROWS}x${SYNTH_COLS} (synthesis), DATA_WIDTH=${DW}, ACC_WIDTH=${AW}\n"
"`timescale 1ns / 1ps\n\n"
"module synth_array (\n"
" input logic clk,\n"
" input logic rst_n")

foreach(r RANGE 0 ${MAX_R})
file(APPEND ${SYNTH_SRC}
",\n input logic signed [${DW}-1:0] data_in_left_${r}")
endforeach()
foreach(r RANGE 0 ${MAX_R})
file(APPEND ${SYNTH_SRC}
",\n output logic signed [${DW}-1:0] data_out_right_${r}")
endforeach()
foreach(c RANGE 0 ${MAX_C})
file(APPEND ${SYNTH_SRC}
",\n input logic weight_ld_${c}")
endforeach()
foreach(c RANGE 0 ${MAX_C})
file(APPEND ${SYNTH_SRC}
",\n input logic signed [${AW}-1:0] acc_in_top_${c}")
endforeach()
foreach(c RANGE 0 ${MAX_C})
file(APPEND ${SYNTH_SRC}
",\n output logic signed [${AW}-1:0] acc_out_bottom_${c}")
endforeach()

file(APPEND ${SYNTH_SRC} "\n);\n\n")

# Data wires across rows (n + 1 columns: 0..COLS)
foreach(c RANGE 0 ${SYNTH_COLS})
foreach(r RANGE 0 ${MAX_R})
file(APPEND ${SYNTH_SRC}
" logic signed [${DW}-1:0] data_wire_${c}_${r};\n")
endforeach()
endforeach()
file(APPEND ${SYNTH_SRC} "\n")

# Acc wires down columns (n + 1 rows per column: 0..ROWS)
foreach(c RANGE 0 ${MAX_C})
foreach(r RANGE 0 ${SYNTH_ROWS})
file(APPEND ${SYNTH_SRC}
" logic signed [${AW}-1:0] acc_wire_${c}_${r};\n")
endforeach()
endforeach()
file(APPEND ${SYNTH_SRC} "\n")

# Left-edge data inputs
foreach(r RANGE 0 ${MAX_R})
file(APPEND ${SYNTH_SRC}
" assign data_wire_0_${r} = data_in_left_${r};\n")
endforeach()
file(APPEND ${SYNTH_SRC} "\n")

# Right-edge data outputs
foreach(r RANGE 0 ${MAX_R})
file(APPEND ${SYNTH_SRC}
" assign data_out_right_${r} = data_wire_${SYNTH_COLS}_${r};\n")
endforeach()
file(APPEND ${SYNTH_SRC} "\n")

# Top-edge acc inputs
foreach(c RANGE 0 ${MAX_C})
file(APPEND ${SYNTH_SRC}
" assign acc_wire_${c}_0 = acc_in_top_${c};\n")
endforeach()
file(APPEND ${SYNTH_SRC} "\n")

# Bottom-edge acc outputs
foreach(c RANGE 0 ${MAX_C})
file(APPEND ${SYNTH_SRC}
" assign acc_out_bottom_${c} = acc_wire_${c}_${SYNTH_ROWS};\n")
endforeach()
file(APPEND ${SYNTH_SRC} "\n")

# Instantiate all PEs
foreach(c RANGE 0 ${MAX_C})
math(EXPR NEXT_C "${c} + 1")
foreach(r RANGE 0 ${MAX_R})
math(EXPR NEXT_R "${r} + 1")
file(APPEND ${SYNTH_SRC}
" pe #(\n"
" .DATA_WIDTH (${DW}),\n"
" .ACC_WIDTH (${AW})\n"
" ) u_pe_${c}_${r} (\n"
" .clk (clk),\n"
" .rst_n (rst_n),\n"
" .weight_ld (weight_ld_${c}),\n"
" .data_in (data_wire_${c}_${r}),\n"
" .data_out (data_wire_${NEXT_C}_${r}),\n"
" .acc_in (acc_wire_${c}_${r}),\n"
" .acc_out (acc_wire_${c}_${NEXT_R})\n"
" );\n")
endforeach()
endforeach()

file(APPEND ${SYNTH_SRC} "\nendmodule\n")

add_custom_command(
OUTPUT ${SYNTH_JSON}
COMMAND ${YOSYS}
-p "read_verilog -sv ${CMAKE_SOURCE_DIR}/src/pe.sv ${SYNTH_SRC}"
-p "hierarchy -check -top synth_array"
-p "flatten"
-p "proc"
-p "opt"
-p "techmap"
-p "opt"
-p "dfflibmap -liberty $ENV{SKYWATER_LIB}"
-p "tee -o ${SYNTH_OUT}/synth_timing.rpt abc -liberty $ENV{SKYWATER_LIB} -D ${CLOCK_PERIOD_PS} -script ${CMAKE_BINARY_DIR}/abc_timing.script"
-p "opt_clean"
-p "write_json ${SYNTH_JSON}"
-p "tee -o ${SYNTH_OUT}/synth_stats.txt stat -width"
DEPENDS
${CMAKE_SOURCE_DIR}/src/pe.sv
${SYNTH_SRC}
${ABC_SCRIPT}
${CLOCK_PERIOD_STAMP}
COMMENT "Synthesising array (${SYNTH_ROWS}x${SYNTH_COLS}) to SkyWater130 cells"
)

add_custom_target(synth DEPENDS ${SYNTH_JSON})
Loading