diff --git a/.gitignore b/.gitignore index 3ffa486..1023a41 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ scripts/*.onnx scripts/*.png .DS_Store __cmake_systeminformation +synth_outputs diff --git a/AGENTS.md b/AGENTS.md index f9fb403..d4c96e3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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}`. @@ -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/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. diff --git a/CMakeLists.txt b/CMakeLists.txt index 9dc73a0..d076197 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/README.md b/README.md index 1af4ba7..37156e7 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 (↓). diff --git a/cmake/synth.cmake b/cmake/synth.cmake new file mode 100644 index 0000000..ec1aedd --- /dev/null +++ b/cmake/synth.cmake @@ -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 +# /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}) diff --git a/scripts/sweep_period.sh b/scripts/sweep_period.sh new file mode 100755 index 0000000..54b4595 --- /dev/null +++ b/scripts/sweep_period.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Sweep CLOCK_PERIOD_NS and tabulate area vs timing tradeoff. +# +# Usage: ./scripts/sweep_period.sh +# +# Synthesises a **single column** (SYNTH_COLS=1) because the critical path is +# the accumulator chain down one column — all columns have identical delay. +# A full 16-column sweep at 2 ns exceeds 10 GB RAM. +# For full-array synthesis, override: cmake -DSYNTH_COLS=16 ... +# +# Results per period are saved to synth_outputs/ns/ and a summary +# table is printed at the end. Estimated full-array area ≈ column_area × 16. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJ_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +BUILD_DIR="${PROJ_DIR}/build" +OUT_DIR="${PROJ_DIR}/synth_outputs" +PERIODS=(2 3 5 10 20) + +echo "================================================================" +echo " Clock Period Sweep" +echo "================================================================" +echo "" + +for period in "${PERIODS[@]}"; do + echo "--- Period=${period}ns ---" + + out_sub="${OUT_DIR}/${period}ns" + mkdir -p "${out_sub}" + + cmake -S "${PROJ_DIR}" -B "${BUILD_DIR}" \ + -DSIM=ON -DCLOCK_PERIOD_NS="${period}" \ + -DSYNTH_ROWS=16 -DSYNTH_COLS=1 > "${out_sub}/cmake.log" 2>&1 + # Force rebuild by removing previous output — make's dependency tracking + # can miss command-line-only changes (the -D flag value in Yosys command). + rm -f "${OUT_DIR}/synth.json" + make -C "${BUILD_DIR}" synth 2>&1 | tee "${out_sub}/build.log" | tail -5 + + cp "${OUT_DIR}/synth_stats.txt" "${out_sub}/synth_stats.txt" + cp "${OUT_DIR}/synth_timing.rpt" "${out_sub}/synth_timing.rpt" + cp "${OUT_DIR}/synth.json" "${out_sub}/synth.json" + + echo "" +done + +# ---- Extract & tabulate key metrics ---- +echo "" +echo "================================================================" +echo " Summary: Area vs Frequency Tradeoff (${#PERIODS[@]} periods)" +echo " 1 column (SYNTH_COLS=1) — multiply area ×16 for full 16-col array" +echo "================================================================" +printf "%-10s %-10s %-10s %-12s %-12s %-12s\n" "Period" "Freq" "Cells" "GateArea" "Area×16" "Delay" +printf "%-10s %-10s %-10s %-12s %-12s %-12s\n" "(ns)" "(MHz)" "" "" "" "(ps)" +printf "%-10s %-10s %-10s %-12s %-12s %-12s\n" "----------" "----------" "----------" "------------" "------------" "------------" + +for period in "${PERIODS[@]}"; do + stats="${OUT_DIR}/${period}ns/synth_stats.txt" + rpt="${OUT_DIR}/${period}ns/synth_timing.rpt" + + total_cells=$(awk '/cells$/ {print $1; exit}' "${stats}") + # The ABC summary line looks like: + # ABC: WireLoad = "none" Gates = 8074 ... Area = 57935.56 ... Delay = 10483.05 ps + abc_line=$(grep -F 'WireLoad' "${rpt}" || true) + area=$(echo "${abc_line}" | sed -n 's/.*Area *= *\([0-9.]*\).*/\1/p') + delay=$(echo "${abc_line}" | sed -n 's/.*Delay *= *\([0-9.]*\).*/\1/p') + delay="${delay:-?}" + freq_mhz=$(python3 -c "print(round(1000 / ${period}, 1))") + + if [ -n "${area}" ]; then + area_x16=$(python3 -c "print(round(${area} * 16, 2))") + else + area_x16="?" + fi + + printf "%-10s %-10s %-10s %-12s %-12s %-12s\n" \ + "${period} ns" "${freq_mhz}" "${total_cells}" "${area}" "${area_x16}" "${delay}" +done +echo ""