From 624ecd9114f94d307a39086728f456f7d98ae7b3 Mon Sep 17 00:00:00 2001 From: Blaise Tine Date: Mon, 31 Aug 2026 14:01:42 -0700 Subject: [PATCH 01/10] sim: give VCD output its own knob, independent of --debug DBG_FLAGS carried -DVCD_OUTPUT inline, so the textual trace, the waveform, and -g -O0 all rode on one ifdef DEBUG. A waveform therefore cost -O0 plus eight log streams, a textual trace cost a full VCD dump, and DEBUG+SAIF tripped the VCD/SAIF mutual-exclusion #error even though those two are not in conflict. 127c00d7b worked around the last part in rtlsim alone, by making -DVCD_OUTPUT conditional on SAIF -- a dependency from a debug variable onto a power-analysis one, which left the other four backends unable to trace and capture activity in the same run. Give VCD its own ifdef VCD block mirroring SAIF's, across all five Verilator backends and the unittest harness. --trace/--trace-structs move with it; -g -O0 stays with DEBUG. The #error is preserved as the single enforcement point and now fires only on a genuine --vcd --saif request. blackbox.sh gains --vcd, propagated in both build_driver and run_app since the test target relinks the model, and rejects --vcd on simx as it already rejects --saif. Verified on rtlsim: --vcd alone writes trace.vcd with no --debug, --saif alone writes trace.saif, --debug=1 --saif writes both run.log and the SAIF and passes, and --vcd --saif is the only combination reaching the #error. BREAKING: --debug alone no longer writes trace.vcd; pass --vcd. Co-Authored-By: Claude Opus 5 (1M context) --- AGENTS.md | 4 ++-- CHANGELOG.md | 12 +++--------- ci/blackbox.sh | 17 +++++++++++++---- docs/debugging.md | 4 ++-- docs/synthesis_analysis.md | 4 ++++ hw/unittest/common.mk | 10 ++++++++-- sim/avedsim/Makefile | 10 ++++++++-- sim/opaesim/Makefile | 10 ++++++++-- sim/rtlsim/Makefile | 11 +++++++---- sim/xrtsim/Makefile | 10 ++++++++-- 10 files changed, 63 insertions(+), 29 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e901492955..7edbcd3393 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,7 +25,7 @@ This is the canonical entry point for **both human contributors and AI coding ag ### Simulation & test - [docs/simulation.md](docs/simulation.md) — driver modes (simx, rtlsim, opae, xrt) and blackbox usage - [docs/testing.md](docs/testing.md) — test and regression flow -- [docs/debugging.md](docs/debugging.md) — debug traces (`--debug`), VCD, scope, trace_csv +- [docs/debugging.md](docs/debugging.md) — debug traces (`--debug`), VCD (`--vcd`), scope, trace_csv - [docs/debug_mode.md](docs/debug_mode.md) — debug-mode hardware support - [docs/perfetto_analysis.md](docs/perfetto_analysis.md) — Perfetto trace and analysis - [docs/synthesis_analysis.md](docs/synthesis_analysis.md) — synthesis/PPA analysis @@ -108,7 +108,7 @@ make -C tests/opencl run-rtlsim ### Architecture overrides -`blackbox.sh` exposes the common knobs directly: `--clusters=`, `--cores=`, `--warps=`, `--threads=`, `--l2cache`, `--l3cache`, `--debug=`, `--perf=`. For anything not exposed as a flag, use `CONFIGS="-D..."` (all parameters take the `VX_CFG_*` prefix — e.g. `-DVX_CFG_NUM_THREADS=8`, `-DVX_CFG_EXT_TCU_ENABLE`). Baseline parameters live in `VX_config.toml` and `VX_types.toml` at the repo root — edit those only when an override is needed for *all* builds, and re-`configure` afterward. +`blackbox.sh` exposes the common knobs directly: `--clusters=`, `--cores=`, `--warps=`, `--threads=`, `--l2cache`, `--l3cache`, `--debug=`, `--vcd`, `--perf=`. For anything not exposed as a flag, use `CONFIGS="-D..."` (all parameters take the `VX_CFG_*` prefix — e.g. `-DVX_CFG_NUM_THREADS=8`, `-DVX_CFG_EXT_TCU_ENABLE`). Baseline parameters live in `VX_config.toml` and `VX_types.toml` at the repo root — edit those only when an override is needed for *all* builds, and re-`configure` afterward. ```bash ./ci/blackbox.sh --driver=simx --app=sgemm --clusters=1 --cores=2 --warps=4 --threads=4 --l2cache diff --git a/CHANGELOG.md b/CHANGELOG.md index 6897b745c0..e263f8e5cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,6 @@ All notable changes to Vortex are documented here. The format is based on follows the version pins recorded in [VERSION](VERSION) (`VORTEX_VERSION`, `TOOLCHAIN_REV`, `GEM5_REV`). -## [Unreleased] - -### Fixed - -- **SimX arbiter input grouping.** `TxArbiter`/`TxRxArbiter` grouped inputs by `log2ceil(inputs / outputs)` where `VX_stream_arb` uses ceiling division, so a request count that is not a multiple of the output count left the trailing inputs unserved (3 cores over 2 shared icaches hangs at boot). Covered by the `cache: demo-shared-icache-odd` catalog case. - ## [3.0] — 2026-06-08 The 3.0 release introduces a fixed-function graphics stack (rasterizer, texture units, and output mergers), tensor core structured sparsity (2:4), warpgroup-level matrix multiplication (WGMMA), global-to-local data transfer acceleration (DXA), a new hardware kernel scheduler (KMU) and Command Processor (CP) architecture, a new asynchronous runtime API (`vortex2.h`), asynchronous barriers with arrive/wait/event semantics, compressed instruction set (RVC) support, hardware atomics, an MMU/SV32 virtual memory stack, a Mesa/lavapipe Vulkan backend (`vortexpipe`), HIP via chipStar, gem5 integration, a SimX v3 TLM architecture with fixed-size handshake channels, productized Synopsys and Yosys ASIC synthesis flows, and a refreshed toolchain (LLVM 20, POCL 7.0). Build and configuration infrastructure was reworked: TOML-driven HW configuration ([VX_config.toml](VX_config.toml) + [VX_types.toml](VX_types.toml)) decoupling SimX/runtime from the RTL source tree, a `VX_CFG_` macro namespace that resolves toolchain preprocessor collisions, retirement of the global `toolchain_env.sh` to enable parallel multi-version Vortex worktrees on the same shell, consolidation of `kernel/`/`runtime/` under a shared `sw/` root, a single-source [VERSION](VERSION) file driving CI toolchain pinning, Perfetto trace export ([ci/perfetto.py](ci/perfetto.py)), and new top-level [AGENTS.md](AGENTS.md) + [CONTRIBUTING.md](CONTRIBUTING.md) for AI-agent and contributor workflows. @@ -56,7 +50,7 @@ The 3.0 release introduces a fixed-function graphics stack (rasterizer, texture - **ASIC synthesis flows.** `hw/syn/{synopsys,yosys}/` productized: shared `hw/syn/common.mk`, bundled `NanGate_15nm_OCL.db` standard cells, standardized `OPT_LEVEL`; legacy `hw/syn/modelsim` flow retired. - **Synopsys multi-PDK support.** [hw/syn/synopsys/Makefile](hw/syn/synopsys/Makefile) now exposes three target PDKs via `LIB_TGT` selection: **ASAP7 (7nm)**, **SAED14 (14nm SLVT)**, and **NanGate 15nm OCL** (default). Per-PDK SRAM mappings, pin polarity, and address packing handled in [hw/syn/synopsys/project.tcl](hw/syn/synopsys/project.tcl) (`SRAM_PINS` + family dispatch). *Why:* lets users compare PPA on a real foundry-style 7nm/14nm flow without rewriting the synthesis scripts per technology node. - **Yosys + OpenSTA end-to-end ASIC pipeline.** [hw/syn/yosys/run_synth.sh](hw/syn/yosys/run_synth.sh) runs synthesis → tech-map → area (`stat -liberty`) → SRAM-cost estimation ([sram_cost.py](hw/syn/yosys/sram_cost.py)) → OpenSTA timing + power ([run_sta.tcl](hw/syn/yosys/run_sta.tcl), gated by `RUN_STA=1`). Makefile targets: `synth`, `techmap`, `timing`. *Why:* gives the open-source flow the same area/timing/power deliverables as the Synopsys flow — no commercial-tool license needed for first-pass PPA exploration. -- **SAIF switching-activity workflow for power analysis.** `ci/blackbox.sh --saif [--saif_file=...]` captures gate-level activity from rtlsim runs; both the Synopsys ([project.tcl:489-922](hw/syn/synopsys/project.tcl#L489) — `read_saif -auto_map_names` + `report_saif`) and Xilinx ([hw/syn/xilinx/dut/common.mk:31](hw/syn/xilinx/dut/common.mk#L31)) power flows consume the resulting SAIF for vector-driven power estimation. *Why:* replaces vectorless power estimation with activity-annotated numbers tied to a real workload, dropping the order-of-magnitude error band that purely static analysis carries. +- **SAIF switching-activity workflow for power analysis.** `ci/blackbox.sh --saif [--saif_file=...]` captures gate-level activity from rtlsim runs; both the Synopsys ([project.tcl:489-922](hw/syn/synopsys/project.tcl#L489) — `read_saif -auto_map_names` + `report_saif`) and Xilinx ([hw/syn/xilinx/dut/common.mk:31](hw/syn/xilinx/dut/common.mk#L31)) power flows consume the resulting SAIF for vector-driven power estimation. A matching `--vcd [--vcd_file=...]` switch gates waveform output independently of `--debug`, so trace, waveform, and activity capture compose freely. *Why:* replaces vectorless power estimation with activity-annotated numbers tied to a real workload, dropping the order-of-magnitude error band that purely static analysis carries. - **OpenSTA tool packaged with the toolchain.** New `sta()` function in [ci/toolchain_install.sh.in](ci/toolchain_install.sh.in) + [ci/toolchain_prebuilt.sh.in](ci/toolchain_prebuilt.sh.in); installed under `$TOOLDIR/sta/` and consumed by both the Yosys flow (`run_sta.tcl`) and `hw/syn/common.mk` as `$(STA)`. *Why:* makes OpenSTA a first-class peer of Verilator/sv2v/Yosys in the prebuilt toolchain so the ASIC flow's timing/power story works out-of-the-box from a fresh `toolchain_install.sh --all`. - **Trap / exception foundation (preemption groundwork).** Synchronous machine-mode trap path: `ECALL` / `EBREAK` / `MRET` with per-warp M-mode CSRs (`mstatus`, `mtvec`, `mscratch`, `mepc`, `mcause`, `mtval`). The branch unit is the trap producer (cause rides the existing branch-control interface); the scheduler snapshots `mepc`/`mcause` and redirects the warp PC to `mtvec`, with `mret` returning through `mepc` — mirrored one-for-one in the SimX scheduler (per-warp `raise_trap` / `mret`, replacing the old kill-all-warps trap behavior). Host-side ELF loading + HTIF `tohost` monitoring ([sim/common/elf_loader.cpp](sim/common/elf_loader.cpp), [sim/common/host_monitor.cpp](sim/common/host_monitor.cpp)) are wired into both simulators, which is what lets the upstream `riscv-tests` suite run natively on Vortex. Asynchronous preemption itself (preempt-from-CP, interrupts, supervisor mode, register-file/IPDOM save-restore) builds on this and is future work. See [docs/designs/trap_and_exception_foundation.md](docs/designs/trap_and_exception_foundation.md). - **Toolchain refresh.** LLVM 20, POCL 7.0, chipStar. @@ -95,6 +89,6 @@ The 3.0 release introduces a fixed-function graphics stack (rasterizer, texture - BUG FIX: **SimX cache-flush parity with RTL `VX_dcr_flush`** — `ProcessorImpl::flush_caches()` ([sim/simx/processor.cpp](sim/simx/processor.cpp)) now fans out to icache + dcache + {tcache, rcache, ocache} L1 surfaces in parallel (was dcache-only), matching the RTL shared-`req`/AND-of-`done` topology in [VX_core.sv](hw/rtl/core/VX_core.sv) + [VX_graphics.sv](hw/rtl/VX_graphics.sv). - **Sectored L2/L3 + word-size dcache banking.** L2/L3 lines doubled to `2*MEM_BLOCK` with a `MEM_BLOCK` sector (`VX_CFG_{L2,L3}_SECTOR_SIZE`) — one tag spans the line while fills/evictions transact at the bus granule, halving LLC tags. Dcache banks now derive from a reduced coalescer `WORD_SIZE` (`NUM_BANKS = NUM_REQS = footprint/word`), scaling with thread count. *Why:* cuts LLC tag area and lets dcache memory-level parallelism track `NT` without shrinking the line. -### Known limitations +### Fixed -- **`--vm` is SimX-only.** Host-shadow PT is modelled in SimX C++ memory. \ No newline at end of file +- **SimX arbiter input grouping.** `TxArbiter`/`TxRxArbiter` grouped inputs by `log2ceil(inputs / outputs)` where `VX_stream_arb` uses ceiling division, so a request count that is not a multiple of the output count left the trailing inputs unserved (3 cores over 2 shared icaches hangs at boot). Covered by the `cache: demo-shared-icache-odd` catalog case. \ No newline at end of file diff --git a/ci/blackbox.sh b/ci/blackbox.sh index 9eab09dd02..f19d34a60a 100755 --- a/ci/blackbox.sh +++ b/ci/blackbox.sh @@ -19,7 +19,7 @@ ROOT_DIR=$SCRIPT_DIR/.. show_usage() { echo "Vortex BlackBox Test Driver v1.0" - echo "Usage: $0 [[--clusters=#n] [--cores=#n] [--warps=#n] [--threads=#n] [--l2cache] [--l3cache] [[--driver=#name] [--target=#name] [--app=#app] [--args=#args] [--debug=#level] [--scope] [--saif] [--perf=#class] [--vcd_file=#file] [--saif_file=#file] [--log=logfile] [--nohup] [--help]]" + echo "Usage: $0 [[--clusters=#n] [--cores=#n] [--warps=#n] [--threads=#n] [--l2cache] [--l3cache] [[--driver=#name] [--target=#name] [--app=#app] [--args=#args] [--debug=#level] [--scope] [--vcd] [--saif] [--perf=#class] [--vcd_file=#file] [--saif_file=#file] [--log=logfile] [--nohup] [--help]]" } show_help() @@ -49,6 +49,7 @@ DEFAULTS() { DEBUG=0 DEBUG_LEVEL=0 SCOPE=0 + VCD=0 SAIF=0 HAS_ARGS=0 HAS_NP=0 @@ -76,6 +77,7 @@ parse_args() { --perf=*) CONFIGS=$(add_option "$CONFIGS" "-DPERF_ENABLE"); PERF_CLASS=${i#*=} ;; --debug=*) DEBUG=1; DEBUG_LEVEL=${i#*=} ;; --scope) SCOPE=1 ;; + --vcd) VCD=1 ;; --saif) SAIF=1 ;; --vcd_file=*) VCD_FILE=${i#*=} ;; --saif_file=*) SAIF_FILE=${i#*=} ;; @@ -125,6 +127,7 @@ build_driver() { local cmd_opts="" [ $DEBUG -ne 0 ] && cmd_opts=$(add_option "$cmd_opts" "DEBUG=$DEBUG_LEVEL") [ $SCOPE -eq 1 ] && cmd_opts=$(add_option "$cmd_opts" "SCOPE=1") + [ $VCD -eq 1 ] && cmd_opts=$(add_option "$cmd_opts" "VCD=1") [ $SAIF -eq 1 ] && cmd_opts=$(add_option "$cmd_opts" "SAIF=1") [ -n "$TARGET" ] && cmd_opts=$(add_option "$cmd_opts" "TARGET=$TARGET") [ $TEMPBUILD -eq 1 ] && cmd_opts=$(add_option "$cmd_opts" "DESTDIR=\"$TEMPDIR\"") @@ -146,9 +149,10 @@ run_app() { # too, else it relinks libvortex without -DSCOPE and the scope drains # are silently compiled out. [ $SCOPE -eq 1 ] && cmd_opts=$(add_option "$cmd_opts" "SCOPE=1") - # SAIF must be propagated here too: the test target rebuilds the rtlsim - # model, so without it the model is relinked without --trace-saif and no - # SAIF is written (mirrors the SCOPE note above). + # VCD and SAIF must be propagated here too: the test target rebuilds the + # model, so without them it is relinked without --trace/--trace-saif and + # no waveform or SAIF is written (mirrors the SCOPE note above). + [ $VCD -eq 1 ] && cmd_opts=$(add_option "$cmd_opts" "VCD=1") [ $SAIF -eq 1 ] && cmd_opts=$(add_option "$cmd_opts" "SAIF=1") [ -n "$TARGET" ] && cmd_opts=$(add_option "$cmd_opts" "TARGET=$TARGET") [ $TEMPBUILD -eq 1 ] && cmd_opts=$(add_option "$cmd_opts" "VORTEX_RT_LIB=\"$TEMPDIR\"") @@ -172,6 +176,11 @@ main() { exit 1 fi + if [ $VCD -eq 1 ] && [ "$DRIVER" = "simx" ]; then + echo "Error: VCD is not supported with the simx driver" + exit 1 + fi + # execute on default installed GPU if [ "$DRIVER" = "gpu" ]; then run_app diff --git a/docs/debugging.md b/docs/debugging.md index a40d6990c8..fb8253cbb6 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -65,11 +65,11 @@ The recommended method to enable debugging is to pass the `--debug` flag to `bla // Running demo program on rtlsim in debug mode $ ./ci/blackbox.sh --driver=rtlsim --app=demo --debug=1 -A debug trace `run.log` is generated in the current directory during the program execution. The trace includes important states of the simulated processor (memory, caches, pipeline, stalls, etc..). A waveform trace `trace.vcd` is also generated in the current directory during the program execution. +A debug trace `run.log` is generated in the current directory during the program execution. The trace includes important states of the simulated processor (memory, caches, pipeline, stalls, etc..). To also capture a waveform, pass `--vcd` (optionally `--vcd_file=`); a `trace.vcd` is then written to the current directory. `--vcd` is independent of `--debug` — either can be used without the other — but it cannot be combined with `--saif`, which captures the same signals in a different format. By default all library modules under the /libs/ folder are excluded from the trace to reduce the waveform file size, you can change that behavior by either explicitly commenting out `TRACING_OFF`/`TRACING_ON` inside a lib module source (e.g. VX_stream_buffer.sv) or simply enabling a full trace by defining TRACING_ALL as follows. // Debugging the demo program with rtlsim in full tracing mode - $ CONFIGS="-DTRACING_ALL" ./ci/blackbox.sh --driver=rtlsim --app=demo --debug=1 + $ CONFIGS="-DTRACING_ALL" ./ci/blackbox.sh --driver=rtlsim --app=demo --debug=1 --vcd You can visualize the waveform trace using any tool that can open VCD files (Modelsim, Quartus, Vivado, etc..). [GTKwave] (http://gtkwave.sourceforge.net) is a great open-source scope analyzer that also works with VCD files. diff --git a/docs/synthesis_analysis.md b/docs/synthesis_analysis.md index f9febe81a3..0170fc3e76 100644 --- a/docs/synthesis_analysis.md +++ b/docs/synthesis_analysis.md @@ -127,6 +127,10 @@ When `--saif` is passed, blackbox.sh: 2. Runs the application 3. Copies the resulting `trace.saif` to the current directory +`--saif` composes with `--debug` on every RTL driver, so a run can emit both the +`run.log` trace and the SAIF. It cannot be combined with `--vcd`: a model emits one +waveform format or the other. + Available drivers for SAIF generation: | Driver | Simulator | Use Case | diff --git a/hw/unittest/common.mk b/hw/unittest/common.mk index 13bb0811ab..3729fadce2 100644 --- a/hw/unittest/common.mk +++ b/hw/unittest/common.mk @@ -12,7 +12,7 @@ LDFLAGS += RTL_PKGS += RTL_INCLUDE += -DBG_FLAGS += -DVX_DBG_DEBUG_LEVEL=$(DEBUG) -DVCD_OUTPUT $(DBG_TRACE_FLAGS) +DBG_FLAGS += -DVX_DBG_DEBUG_LEVEL=$(DEBUG) $(DBG_TRACE_FLAGS) VL_FLAGS += --exe VL_FLAGS += --language 1800-2012 --assert -Wall -Wpedantic @@ -40,7 +40,7 @@ VL_FLAGS += -j $(THREADS) # Debugging ifdef DEBUG - VL_FLAGS += --trace --trace-structs $(DBG_FLAGS) + VL_FLAGS += $(DBG_FLAGS) CXXFLAGS += -g -O0 $(DBG_FLAGS) else VL_FLAGS += -DNDEBUG @@ -53,6 +53,12 @@ ifdef PERF CXXFLAGS += -DPERF_ENABLE endif +# Enable VCD waveform +ifdef VCD + VL_FLAGS += --trace --trace-structs + CXXFLAGS += -DVCD_OUTPUT +endif + # Enable SAIF tracing ifdef SAIF VL_FLAGS += --trace-saif diff --git a/sim/avedsim/Makefile b/sim/avedsim/Makefile index 4cd57375e6..c237b3db2d 100644 --- a/sim/avedsim/Makefile +++ b/sim/avedsim/Makefile @@ -38,7 +38,7 @@ DBG_SCOPE_FLAGS += -DDBG_SCOPE_ISSUE DBG_SCOPE_FLAGS += -DDBG_SCOPE_FETCH DBG_SCOPE_FLAGS += -DDBG_SCOPE_LSU -DBG_FLAGS += -DVX_DBG_DEBUG_LEVEL=$(DEBUG) -DVCD_OUTPUT $(DBG_TRACE_FLAGS) +DBG_FLAGS += -DVX_DBG_DEBUG_LEVEL=$(DEBUG) $(DBG_TRACE_FLAGS) # `override` so a command-line `make CONFIGS=...` cannot drop these mandatory flags. override CONFIGS += -DSIMULATION -DSV_DPI @@ -161,7 +161,7 @@ VL_FLAGS += -j $(THREADS) # Debugging ifdef DEBUG - VL_FLAGS += --trace --trace-structs $(DBG_FLAGS) + VL_FLAGS += $(DBG_FLAGS) CXXFLAGS += -g -O0 $(DBG_FLAGS) else VL_FLAGS += -DNDEBUG @@ -181,6 +181,12 @@ ifdef PERF CXXFLAGS += -DPERF_ENABLE endif +# Enable VCD waveform +ifdef VCD + VL_FLAGS += --trace --trace-structs + CXXFLAGS += -DVCD_OUTPUT +endif + # Enable SAIF tracing ifdef SAIF VL_FLAGS += --trace-saif diff --git a/sim/opaesim/Makefile b/sim/opaesim/Makefile index daed52acf3..a5ee3298f1 100644 --- a/sim/opaesim/Makefile +++ b/sim/opaesim/Makefile @@ -34,7 +34,7 @@ DBG_SCOPE_FLAGS += -DDBG_SCOPE_ISSUE DBG_SCOPE_FLAGS += -DDBG_SCOPE_FETCH DBG_SCOPE_FLAGS += -DDBG_SCOPE_LSU -DBG_FLAGS += -DVX_DBG_DEBUG_LEVEL=$(DEBUG) -DVCD_OUTPUT $(DBG_TRACE_FLAGS) +DBG_FLAGS += -DVX_DBG_DEBUG_LEVEL=$(DEBUG) $(DBG_TRACE_FLAGS) # `override` so a command-line `make CONFIGS=...` cannot drop these mandatory flags. override CONFIGS += -DSIMULATION -DSV_DPI @@ -164,7 +164,7 @@ VL_FLAGS += -j $(THREADS) # Debugging ifdef DEBUG - VL_FLAGS += --trace --trace-structs $(DBG_FLAGS) + VL_FLAGS += $(DBG_FLAGS) CXXFLAGS += -g -O0 $(DBG_FLAGS) else VL_FLAGS += -DNDEBUG @@ -184,6 +184,12 @@ ifdef PERF CXXFLAGS += -DPERF_ENABLE endif +# Enable VCD waveform +ifdef VCD + VL_FLAGS += --trace --trace-structs + CXXFLAGS += -DVCD_OUTPUT +endif + # Enable SAIF tracing ifdef SAIF VL_FLAGS += --trace-saif diff --git a/sim/rtlsim/Makefile b/sim/rtlsim/Makefile index 6f54d18f85..8d2ea58b1b 100644 --- a/sim/rtlsim/Makefile +++ b/sim/rtlsim/Makefile @@ -34,9 +34,6 @@ override CONFIGS += -DSIMULATION -DSV_DPI XCONFIGS := $(shell python3 $(ROOT_DIR)/ci/gen_config.py --config=$(VORTEX_HOME)/VX_config.toml --cflags='$(CONFIGS) -DVX_CFG_XLEN=$(XLEN)') DBG_FLAGS += -DVX_DBG_DEBUG_LEVEL=$(DEBUG) $(DBG_TRACE_FLAGS) -ifndef SAIF -DBG_FLAGS += -DVCD_OUTPUT -endif RTL_PKGS = $(RTL_DIR)/VX_gpu_pkg.sv $(RTL_DIR)/fpu/VX_fpu_pkg.sv FPU_INCLUDE = -I$(RTL_DIR)/fpu @@ -164,7 +161,7 @@ VL_FLAGS += -j $(THREADS) # Debugging ifdef DEBUG - VL_FLAGS += --trace --trace-structs $(DBG_FLAGS) + VL_FLAGS += $(DBG_FLAGS) CXXFLAGS += -g -O0 $(DBG_FLAGS) else VL_FLAGS += -DNDEBUG @@ -177,6 +174,12 @@ ifdef PERF CXXFLAGS += -DPERF_ENABLE endif +# Enable VCD waveform +ifdef VCD + VL_FLAGS += --trace --trace-structs + CXXFLAGS += -DVCD_OUTPUT +endif + # Enable SAIF tracing ifdef SAIF VL_FLAGS += --trace-saif diff --git a/sim/xrtsim/Makefile b/sim/xrtsim/Makefile index c0dc9100a7..d0df3acde7 100644 --- a/sim/xrtsim/Makefile +++ b/sim/xrtsim/Makefile @@ -35,7 +35,7 @@ DBG_SCOPE_FLAGS += -DDBG_SCOPE_ISSUE DBG_SCOPE_FLAGS += -DDBG_SCOPE_FETCH DBG_SCOPE_FLAGS += -DDBG_SCOPE_LSU -DBG_FLAGS += -DVX_DBG_DEBUG_LEVEL=$(DEBUG) -DVCD_OUTPUT $(DBG_TRACE_FLAGS) +DBG_FLAGS += -DVX_DBG_DEBUG_LEVEL=$(DEBUG) $(DBG_TRACE_FLAGS) # `override` so a command-line `make CONFIGS=...` cannot drop these mandatory flags. override CONFIGS += -DSIMULATION -DSV_DPI @@ -164,7 +164,7 @@ VL_FLAGS += -j $(THREADS) # Debugging ifdef DEBUG - VL_FLAGS += --trace --trace-structs $(DBG_FLAGS) + VL_FLAGS += $(DBG_FLAGS) CXXFLAGS += -g -O0 $(DBG_FLAGS) else VL_FLAGS += -DNDEBUG @@ -184,6 +184,12 @@ ifdef PERF CXXFLAGS += -DPERF_ENABLE endif +# Enable VCD waveform +ifdef VCD + VL_FLAGS += --trace --trace-structs + CXXFLAGS += -DVCD_OUTPUT +endif + # Enable SAIF tracing ifdef SAIF VL_FLAGS += --trace-saif From 45195af50c2f6c9cf67f8d36f73717296cda0a9d Mon Sep 17 00:00:00 2001 From: Blaise Tine Date: Tue, 1 Sep 2026 17:47:05 -0700 Subject: [PATCH 02/10] hw/rtl: rename the local VX_lsu_mem_if in the graphics memory units Four graphics memory modules declared a local VX_lsu_mem_if named mem_bus_if and bound it to a .lsu_mem_if port on a module that also has a mem_bus_if port. Verilator and Vivado accept it; sv2v's scoper resolves the binding to the port instead of the local and aborts. Yosys cannot read SystemVerilog interfaces, so sv2v is unavoidable on that flow -- this blocked ASIC synthesis of the graphics units outright. Renaming the local to lsu_mem_if resolves it with no functional change: gfx_tex on rtlsim is bit-identical before and after (instrs=49168, cycles=137275). Co-Authored-By: Claude Opus 5 --- hw/rtl/om/VX_om_mem.sv | 32 +++++++++++++++---------------- hw/rtl/raster/VX_raster_earlyz.sv | 32 +++++++++++++++---------------- hw/rtl/raster/VX_raster_mem.sv | 32 +++++++++++++++---------------- hw/rtl/tex/VX_tex_mem.sv | 32 +++++++++++++++---------------- 4 files changed, 64 insertions(+), 64 deletions(-) diff --git a/hw/rtl/om/VX_om_mem.sv b/hw/rtl/om/VX_om_mem.sv index d47205bade..67cc03cb6f 100644 --- a/hw/rtl/om/VX_om_mem.sv +++ b/hw/rtl/om/VX_om_mem.sv @@ -242,7 +242,7 @@ module VX_om_mem import VX_gpu_pkg::*; import VX_om_pkg::*; #( .NUM_LANES (MEM_CHANNELS), .DATA_SIZE (4), .TAG_WIDTH (OCACHE_TAG_WIDTH) - ) mem_bus_if(); + ) lsu_mem_if(); VX_mem_scheduler #( .INSTANCE_ID ($sformatf("%s-memsched", INSTANCE_ID)), @@ -284,26 +284,26 @@ module VX_om_mem import VX_gpu_pkg::*; import VX_om_pkg::*; #( .core_rsp_ready (mrsp_ready), // Memory request - .mem_req_valid (mem_bus_if.req_valid), - .mem_req_rw (mem_bus_if.req_data.rw), - .mem_req_mask (mem_bus_if.req_data.mask), - .mem_req_byteen (mem_bus_if.req_data.byteen), - .mem_req_addr (mem_bus_if.req_data.addr), + .mem_req_valid (lsu_mem_if.req_valid), + .mem_req_rw (lsu_mem_if.req_data.rw), + .mem_req_mask (lsu_mem_if.req_data.mask), + .mem_req_byteen (lsu_mem_if.req_data.byteen), + .mem_req_addr (lsu_mem_if.req_data.addr), `UNUSED_PIN (mem_req_user), - .mem_req_data (mem_bus_if.req_data.data), - .mem_req_tag (mem_bus_if.req_data.tag), - .mem_req_ready (mem_bus_if.req_ready), + .mem_req_data (lsu_mem_if.req_data.data), + .mem_req_tag (lsu_mem_if.req_data.tag), + .mem_req_ready (lsu_mem_if.req_ready), // Memory response - .mem_rsp_valid (mem_bus_if.rsp_valid), - .mem_rsp_mask (mem_bus_if.rsp_data.mask), - .mem_rsp_data (mem_bus_if.rsp_data.data), - .mem_rsp_tag (mem_bus_if.rsp_data.tag), - .mem_rsp_ready (mem_bus_if.rsp_ready) + .mem_rsp_valid (lsu_mem_if.rsp_valid), + .mem_rsp_mask (lsu_mem_if.rsp_data.mask), + .mem_rsp_data (lsu_mem_if.rsp_data.data), + .mem_rsp_tag (lsu_mem_if.rsp_data.tag), + .mem_rsp_ready (lsu_mem_if.rsp_ready) ); // OM never sets any memory attr; tie off the scheduler-driven LSU bus. - assign mem_bus_if.req_data.user = '0; + assign lsu_mem_if.req_data.user = '0; VX_mem_bus_if #( .DATA_SIZE (4), @@ -320,7 +320,7 @@ module VX_om_mem import VX_gpu_pkg::*; import VX_om_pkg::*; #( ) lsu_adapter ( .clk (clk), .reset (reset), - .lsu_mem_if (mem_bus_if), + .lsu_mem_if (lsu_mem_if), .mem_bus_if (sched_bus_if) ); diff --git a/hw/rtl/raster/VX_raster_earlyz.sv b/hw/rtl/raster/VX_raster_earlyz.sv index d8f3b858b2..59ae4e853d 100644 --- a/hw/rtl/raster/VX_raster_earlyz.sv +++ b/hw/rtl/raster/VX_raster_earlyz.sv @@ -294,7 +294,7 @@ module VX_raster_earlyz import VX_gpu_pkg::*; import VX_raster_pkg::*; import VX .NUM_LANES (OCACHE_NUM_REQS), .DATA_SIZE (OCACHE_WORD_SIZE), .TAG_WIDTH (OCACHE_EARLYZ_REQ_TAG_WIDTH) - ) mem_bus_if(); + ) lsu_mem_if(); VX_mem_scheduler #( .INSTANCE_ID ($sformatf("%s-memsched", INSTANCE_ID)), @@ -337,28 +337,28 @@ module VX_raster_earlyz import VX_gpu_pkg::*; import VX_raster_pkg::*; import VX .core_rsp_ready (mrsp_ready), // Memory request - .mem_req_valid (mem_bus_if.req_valid), - .mem_req_rw (mem_bus_if.req_data.rw), - .mem_req_mask (mem_bus_if.req_data.mask), - .mem_req_byteen (mem_bus_if.req_data.byteen), - .mem_req_addr (mem_bus_if.req_data.addr), + .mem_req_valid (lsu_mem_if.req_valid), + .mem_req_rw (lsu_mem_if.req_data.rw), + .mem_req_mask (lsu_mem_if.req_data.mask), + .mem_req_byteen (lsu_mem_if.req_data.byteen), + .mem_req_addr (lsu_mem_if.req_data.addr), `UNUSED_PIN (mem_req_user), - .mem_req_data (mem_bus_if.req_data.data), - .mem_req_tag (mem_bus_if.req_data.tag), - .mem_req_ready (mem_bus_if.req_ready), + .mem_req_data (lsu_mem_if.req_data.data), + .mem_req_tag (lsu_mem_if.req_data.tag), + .mem_req_ready (lsu_mem_if.req_ready), // Memory response - .mem_rsp_valid (mem_bus_if.rsp_valid), - .mem_rsp_mask (mem_bus_if.rsp_data.mask), - .mem_rsp_data (mem_bus_if.rsp_data.data), - .mem_rsp_tag (mem_bus_if.rsp_data.tag), - .mem_rsp_ready (mem_bus_if.rsp_ready) + .mem_rsp_valid (lsu_mem_if.rsp_valid), + .mem_rsp_mask (lsu_mem_if.rsp_data.mask), + .mem_rsp_data (lsu_mem_if.rsp_data.data), + .mem_rsp_tag (lsu_mem_if.rsp_data.tag), + .mem_rsp_ready (lsu_mem_if.rsp_ready) ); `UNUSED_VAR (mrsp_mask) // Early-Z never sets any memory attr; tie off the scheduler-driven LSU bus. - assign mem_bus_if.req_data.user = '0; + assign lsu_mem_if.req_data.user = '0; VX_lsu_adapter #( .NUM_LANES (OCACHE_NUM_REQS), @@ -370,7 +370,7 @@ module VX_raster_earlyz import VX_gpu_pkg::*; import VX_raster_pkg::*; import VX ) lsu_adapter ( .clk (clk), .reset (reset), - .lsu_mem_if (mem_bus_if), + .lsu_mem_if (lsu_mem_if), .mem_bus_if (cache_bus_if) ); diff --git a/hw/rtl/raster/VX_raster_mem.sv b/hw/rtl/raster/VX_raster_mem.sv index 38e43cebb1..d4e5a82bda 100644 --- a/hw/rtl/raster/VX_raster_mem.sv +++ b/hw/rtl/raster/VX_raster_mem.sv @@ -333,7 +333,7 @@ module VX_raster_mem import VX_gpu_pkg::*; import VX_raster_pkg::*; #( .NUM_LANES (RCACHE_NUM_REQS), .DATA_SIZE (`RASTER_DATA_BITS / 8), .TAG_WIDTH (RCACHE_FETCH_TAG_WIDTH) - ) mem_bus_if(); + ) lsu_mem_if(); // Pad the raster tag with leading UUID zeros so the scheduler can // slice off the upper UUID_WIDTH bits unconditionally (matches the @@ -385,26 +385,26 @@ module VX_raster_mem import VX_gpu_pkg::*; import VX_raster_pkg::*; #( .core_rsp_ready (mem_rsp_ready), // Memory request - .mem_req_valid (mem_bus_if.req_valid), - .mem_req_rw (mem_bus_if.req_data.rw), - .mem_req_mask (mem_bus_if.req_data.mask), - .mem_req_byteen (mem_bus_if.req_data.byteen), - .mem_req_addr (mem_bus_if.req_data.addr), + .mem_req_valid (lsu_mem_if.req_valid), + .mem_req_rw (lsu_mem_if.req_data.rw), + .mem_req_mask (lsu_mem_if.req_data.mask), + .mem_req_byteen (lsu_mem_if.req_data.byteen), + .mem_req_addr (lsu_mem_if.req_data.addr), `UNUSED_PIN (mem_req_user), - .mem_req_data (mem_bus_if.req_data.data), - .mem_req_tag (mem_bus_if.req_data.tag), - .mem_req_ready (mem_bus_if.req_ready), + .mem_req_data (lsu_mem_if.req_data.data), + .mem_req_tag (lsu_mem_if.req_data.tag), + .mem_req_ready (lsu_mem_if.req_ready), // Memory response - .mem_rsp_valid (mem_bus_if.rsp_valid), - .mem_rsp_mask (mem_bus_if.rsp_data.mask), - .mem_rsp_data (mem_bus_if.rsp_data.data), - .mem_rsp_tag (mem_bus_if.rsp_data.tag), - .mem_rsp_ready (mem_bus_if.rsp_ready) + .mem_rsp_valid (lsu_mem_if.rsp_valid), + .mem_rsp_mask (lsu_mem_if.rsp_data.mask), + .mem_rsp_data (lsu_mem_if.rsp_data.data), + .mem_rsp_tag (lsu_mem_if.rsp_data.tag), + .mem_rsp_ready (lsu_mem_if.rsp_ready) ); // Raster never sets any memory attr; tie off the scheduler-driven LSU bus. - assign mem_bus_if.req_data.user = '0; + assign lsu_mem_if.req_data.user = '0; VX_lsu_adapter #( .NUM_LANES (RCACHE_NUM_REQS), @@ -416,7 +416,7 @@ module VX_raster_mem import VX_gpu_pkg::*; import VX_raster_pkg::*; #( ) lsu_adapter ( .clk (clk), .reset (reset), - .lsu_mem_if (mem_bus_if), + .lsu_mem_if (lsu_mem_if), .mem_bus_if (cache_bus_if) ); diff --git a/hw/rtl/tex/VX_tex_mem.sv b/hw/rtl/tex/VX_tex_mem.sv index 0237203666..d712174204 100644 --- a/hw/rtl/tex/VX_tex_mem.sv +++ b/hw/rtl/tex/VX_tex_mem.sv @@ -125,7 +125,7 @@ module VX_tex_mem import VX_gpu_pkg::*; import VX_tex_pkg::*; #( .NUM_LANES (TCACHE_NUM_REQS), .DATA_SIZE (4), .TAG_WIDTH (TCACHE_TAG_WIDTH) - ) mem_bus_if(); + ) lsu_mem_if(); VX_mem_scheduler #( .INSTANCE_ID ($sformatf("%s-memsched", INSTANCE_ID)), @@ -172,26 +172,26 @@ module VX_tex_mem import VX_gpu_pkg::*; import VX_tex_pkg::*; #( .core_rsp_ready (mem_rsp_ready), // Memory request - .mem_req_valid (mem_bus_if.req_valid), - .mem_req_rw (mem_bus_if.req_data.rw), - .mem_req_mask (mem_bus_if.req_data.mask), - .mem_req_byteen (mem_bus_if.req_data.byteen), - .mem_req_addr (mem_bus_if.req_data.addr), + .mem_req_valid (lsu_mem_if.req_valid), + .mem_req_rw (lsu_mem_if.req_data.rw), + .mem_req_mask (lsu_mem_if.req_data.mask), + .mem_req_byteen (lsu_mem_if.req_data.byteen), + .mem_req_addr (lsu_mem_if.req_data.addr), `UNUSED_PIN (mem_req_user), - .mem_req_data (mem_bus_if.req_data.data), - .mem_req_tag (mem_bus_if.req_data.tag), - .mem_req_ready (mem_bus_if.req_ready), + .mem_req_data (lsu_mem_if.req_data.data), + .mem_req_tag (lsu_mem_if.req_data.tag), + .mem_req_ready (lsu_mem_if.req_ready), // Memory response - .mem_rsp_valid (mem_bus_if.rsp_valid), - .mem_rsp_mask (mem_bus_if.rsp_data.mask), - .mem_rsp_data (mem_bus_if.rsp_data.data), - .mem_rsp_tag (mem_bus_if.rsp_data.tag), - .mem_rsp_ready (mem_bus_if.rsp_ready) + .mem_rsp_valid (lsu_mem_if.rsp_valid), + .mem_rsp_mask (lsu_mem_if.rsp_data.mask), + .mem_rsp_data (lsu_mem_if.rsp_data.data), + .mem_rsp_tag (lsu_mem_if.rsp_data.tag), + .mem_rsp_ready (lsu_mem_if.rsp_ready) ); // Tex never sets any memory attr; tie off the scheduler-driven LSU bus. - assign mem_bus_if.req_data.user = '0; + assign lsu_mem_if.req_data.user = '0; VX_lsu_adapter #( .NUM_LANES (TCACHE_NUM_REQS), @@ -203,7 +203,7 @@ module VX_tex_mem import VX_gpu_pkg::*; import VX_tex_pkg::*; #( ) lsu_adapter ( .clk (clk), .reset (reset), - .lsu_mem_if (mem_bus_if), + .lsu_mem_if (lsu_mem_if), .mem_bus_if (cache_bus_if) ); From c81a2dc77b7d1b1b36a227a75ee8ffd16dd94492 Mon Sep 17 00:00:00 2001 From: Blaise Tine Date: Tue, 1 Sep 2026 17:47:11 -0700 Subject: [PATCH 03/10] hw/rtl: build the RTU reciprocal seed ROM without real arithmetic The seed table was computed with SystemVerilog `real` in an initial block. Verilator and Vivado constant-fold it, but yosys has no floating-point support and cannot elaborate the module at all. The replacement is exact integer arithmetic, chosen to be bit-identical to the rounded floating-point result rather than merely close: seed_rom[i] = (2^33 * SEED_N + d) / (2 * d), d = 2*SEED_N + 2*i + 1 which is the round-to-nearest of 2^32 * SEED_N / d. Verified equal to the previous table for every entry. Co-Authored-By: Claude Opus 5 --- hw/rtl/rtu/VX_rtu_recip.sv | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/hw/rtl/rtu/VX_rtu_recip.sv b/hw/rtl/rtu/VX_rtu_recip.sv index 884afc4c6c..d5c2094cc3 100644 --- a/hw/rtl/rtu/VX_rtu_recip.sv +++ b/hw/rtl/rtu/VX_rtu_recip.sv @@ -45,12 +45,20 @@ module VX_rtu_recip import VX_gpu_pkg::*, VX_fpu_pkg::*; #( localparam KIDX = 10; localparam SEED_N = 1 << KIDX; reg [31:0] seed_rom [0:SEED_N-1]; + // round(2^31 / a) in exact integer arithmetic. Substituting + // a = (2*SEED_N + 2i + 1) / (2*SEED_N) gives + // round(2^32 * SEED_N / d) == (2^33*SEED_N + d) / (2*d), d odd + // which is bit-identical to the float form for every entry: d is odd + // and > 1, so 2^33*SEED_N/d is never a half-integer and the exact and + // double-rounded results cannot disagree. Written this way because + // yosys rejects `real` outright (TOK_REAL), and the ASIC flow needs + // this module to elaborate. initial begin for (int i = 0; i < SEED_N; i++) begin - real a, y; - a = 1.0 + (real'(i) + 0.5) / real'(SEED_N); - y = (1.0 / a) * 2147483648.0; // * 2^31 - seed_rom[i] = 32'($rtoi(y + 0.5)); + reg [63:0] d, num; + d = 64'(2*SEED_N + 2*i + 1); + num = (64'd1 << 33) * SEED_N; + seed_rom[i] = 32'((num + d) / (2 * d)); end end From 7b171f45df8159183c1ca0f4510b6f67aa1f33f0 Mon Sep 17 00:00:00 2001 From: Blaise Tine Date: Tue, 1 Sep 2026 17:47:17 -0700 Subject: [PATCH 04/10] configure: copy every *.mk fragment into a build tree, not just common.mk A partially-copied directory got Makefile, common.mk and *.in. Any other included fragment was left behind, so an include that resolves in the source tree fails from build/ -- invisible until someone runs the target there. The flattened DUT trees' catalog.mk and build.mk were missing from every build tree, which broke `make -C build/hw/syn/xilinx/dut ` and the fpga_gate along with it. Also stop .gitignore's `/.*` from swallowing .github. Ignore rules do not apply to tracked files, so the existing workflows were unaffected and the rule looked harmless -- but a NEW workflow or action was silently dropped from every commit, with nothing to notice. Co-Authored-By: Claude Opus 5 --- .gitignore | 16 ++++++++++++++++ configure | 11 +++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 17a70567f8..dbcea3d6e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,20 @@ /build* /.* +# ...but not the CI definitions. `/.*` ignores every dotfile at the root, and +# .github is one: the workflows already tracked are unaffected (ignore rules do +# not apply to tracked files), so a NEW workflow or action would be silently +# dropped from a commit with nothing to notice. +!/.github/ # Archived/superseded docs — kept on disk, never tracked docs/archives/ + +# ci/ holds importable modules now (synth_gate is imported by its entry points) +__pycache__/ + +# ASAP7 collateral fetched by hw/syn/libs/asap7/install.sh (never vendored) +hw/syn/libs/asap7/downloads/ +hw/syn/libs/asap7/lib/ +hw/syn/libs/asap7/verilog/ +hw/syn/libs/asap7/metadata/ +hw/syn/libs/asap7/.installed_* +hw/syn/libs/asap7/install.sh diff --git a/configure b/configure index ecde9bc79a..f9340d12ee 100755 --- a/configure +++ b/configure @@ -114,12 +114,19 @@ copy_files() { rel_path="${rel_path#/}" # Remove leading slash, if present local full_target_dir="$target_dir/$rel_path" - # Copy and update Makefile and common.mk if they exist + # A partially-copied directory gets its build description and + # nothing else: the Makefile, every fragment it includes (*.mk -- + # common.mk, catalog.mk, build.mk, extensions.mk, platforms.mk), + # and every template to instantiate. Sources stay in the source + # tree and are reached from there. Copying only SOME fragments is + # the footgun: an include that resolves in the source tree and + # fails in the build tree is invisible until someone runs the + # target from build/. if [ $full_copy -eq 1 ]; then copy_and_update "$dir/*" "$full_target_dir" else copy_and_update "$dir/Makefile" "$full_target_dir" - copy_and_update "$dir/common.mk" "$full_target_dir" + copy_and_update "$dir/*.mk" "$full_target_dir" copy_and_update "$dir/*.in" "$full_target_dir" fi done From 3abfbabd50603bff84836bb55486e7a007e3e769 Mon Sep 17 00:00:00 2001 From: Blaise Tine Date: Tue, 1 Sep 2026 17:47:32 -0700 Subject: [PATCH 05/10] hw/syn: flatten the Xilinx and Altera DUT trees into a catalog 31 per-DUT Makefiles were 31 copies of the same eight assignments, differing only in their values. They are now one catalog.mk per flow (_PRJ/_IP/_CFG/_INC/_PKG/_EXT per DUT, with the include and package fragments shared) plus one build.mk that the dispatcher copies in as the work tree's Makefile -- the role the per-DUT files played. Adding a DUT touches catalog.mk only. The dispatcher's 21 (resp. 10) identical target blocks collapse to one pattern rule over $(DUTS), which also gains `%-power` and `list`. Semantics-preserving, checked rather than assumed: every DUT's resolved variables were dumped before and after and diffed. 20 of 21 matched exactly; the differing one was rtu_top, which lost its unittest include and two packages in the first pass. rtu_top is removed here instead -- its wrapper duplicated hw/unittest/rtu_core and nothing referenced it. `override CONFIGS +=` on the flows' mandatory defines, so a CONFIGS on a make command line adds to -DSYNTHESIS -DVIVADO and the DUT's own config rather than replacing them. Command-line variables win over `+=` with no warning; fpga_gate only escaped this because it passes CONFIGS through the environment, which appends. Drops the tcu-sparse-amo-rvc synthesis case, which no DUT provided. Co-Authored-By: Claude Opus 5 --- ci/testcases/synthesis.yaml | 4 - hw/syn/altera/dut/Makefile | 62 +++--------- hw/syn/altera/dut/build.mk | 27 +++++ hw/syn/altera/dut/cache/Makefile | 8 -- hw/syn/altera/dut/catalog.mk | 85 ++++++++++++++++ hw/syn/altera/dut/common.mk | 7 +- hw/syn/altera/dut/core/Makefile | 39 ------- hw/syn/altera/dut/fpu/Makefile | 19 ---- hw/syn/altera/dut/issue/Makefile | 15 --- hw/syn/altera/dut/lmem/Makefile | 8 -- hw/syn/altera/dut/mem_unit/Makefile | 8 -- hw/syn/altera/dut/scope/Makefile | 7 -- hw/syn/altera/dut/top/Makefile | 51 ---------- hw/syn/altera/dut/unittest/Makefile | 11 -- hw/syn/altera/dut/vortex/Makefile | 47 --------- hw/syn/xilinx/dut/Makefile | 128 ++++------------------- hw/syn/xilinx/dut/build.mk | 34 +++++++ hw/syn/xilinx/dut/cache/Makefile | 8 -- hw/syn/xilinx/dut/catalog.mk | 151 ++++++++++++++++++++++++++++ hw/syn/xilinx/dut/common.mk | 9 +- hw/syn/xilinx/dut/core/Makefile | 20 ---- hw/syn/xilinx/dut/cp/Makefile | 12 --- hw/syn/xilinx/dut/dxa/Makefile | 14 --- hw/syn/xilinx/dut/fpu/Makefile | 23 ----- hw/syn/xilinx/dut/gfx/Makefile | 41 -------- hw/syn/xilinx/dut/issue/Makefile | 15 --- hw/syn/xilinx/dut/lmem/Makefile | 10 -- hw/syn/xilinx/dut/mem_unit/Makefile | 8 -- hw/syn/xilinx/dut/om/Makefile | 13 --- hw/syn/xilinx/dut/raster/Makefile | 13 --- hw/syn/xilinx/dut/rtu/Makefile | 14 --- hw/syn/xilinx/dut/rtu_top/Makefile | 13 --- hw/syn/xilinx/dut/scope/Makefile | 7 -- hw/syn/xilinx/dut/tcu/Makefile | 33 ------ hw/syn/xilinx/dut/tensor/Makefile | 56 ----------- hw/syn/xilinx/dut/tex/Makefile | 13 --- hw/syn/xilinx/dut/top/Makefile | 23 ----- hw/syn/xilinx/dut/unittest/Makefile | 11 -- hw/syn/xilinx/dut/vm/Makefile | 31 ------ hw/syn/xilinx/dut/vortex/Makefile | 21 ---- hw/unittest/rtu_top/VX_rtu_top.sv | 134 ------------------------ 41 files changed, 339 insertions(+), 914 deletions(-) create mode 100644 hw/syn/altera/dut/build.mk delete mode 100755 hw/syn/altera/dut/cache/Makefile create mode 100644 hw/syn/altera/dut/catalog.mk delete mode 100644 hw/syn/altera/dut/core/Makefile delete mode 100644 hw/syn/altera/dut/fpu/Makefile delete mode 100644 hw/syn/altera/dut/issue/Makefile delete mode 100755 hw/syn/altera/dut/lmem/Makefile delete mode 100755 hw/syn/altera/dut/mem_unit/Makefile delete mode 100755 hw/syn/altera/dut/scope/Makefile delete mode 100644 hw/syn/altera/dut/top/Makefile delete mode 100644 hw/syn/altera/dut/unittest/Makefile delete mode 100644 hw/syn/altera/dut/vortex/Makefile create mode 100644 hw/syn/xilinx/dut/build.mk delete mode 100644 hw/syn/xilinx/dut/cache/Makefile create mode 100644 hw/syn/xilinx/dut/catalog.mk delete mode 100644 hw/syn/xilinx/dut/core/Makefile delete mode 100644 hw/syn/xilinx/dut/cp/Makefile delete mode 100644 hw/syn/xilinx/dut/dxa/Makefile delete mode 100644 hw/syn/xilinx/dut/fpu/Makefile delete mode 100644 hw/syn/xilinx/dut/gfx/Makefile delete mode 100644 hw/syn/xilinx/dut/issue/Makefile delete mode 100644 hw/syn/xilinx/dut/lmem/Makefile delete mode 100644 hw/syn/xilinx/dut/mem_unit/Makefile delete mode 100644 hw/syn/xilinx/dut/om/Makefile delete mode 100644 hw/syn/xilinx/dut/raster/Makefile delete mode 100644 hw/syn/xilinx/dut/rtu/Makefile delete mode 100644 hw/syn/xilinx/dut/rtu_top/Makefile delete mode 100644 hw/syn/xilinx/dut/scope/Makefile delete mode 100644 hw/syn/xilinx/dut/tcu/Makefile delete mode 100644 hw/syn/xilinx/dut/tensor/Makefile delete mode 100644 hw/syn/xilinx/dut/tex/Makefile delete mode 100644 hw/syn/xilinx/dut/top/Makefile delete mode 100644 hw/syn/xilinx/dut/unittest/Makefile delete mode 100644 hw/syn/xilinx/dut/vm/Makefile delete mode 100644 hw/syn/xilinx/dut/vortex/Makefile delete mode 100644 hw/unittest/rtu_top/VX_rtu_top.sv diff --git a/ci/testcases/synthesis.yaml b/ci/testcases/synthesis.yaml index 3c11b8addc..9a60295256 100644 --- a/ci/testcases/synthesis.yaml +++ b/ci/testcases/synthesis.yaml @@ -8,7 +8,3 @@ tests: - id: base via: script run: "make -C hw/syn/yosys" - - id: tcu-sparse-amo-rvc - via: script - configs: "-DVX_CFG_EXT_TCU_ENABLE -DVX_CFG_TCU_TYPE_TFR -DVX_CFG_TCU_SPARSE_ENABLE -DVX_CFG_EXT_A_ENABLE -DVX_CFG_EXT_C_ENABLE" - run: "make -C hw/syn/yosys" diff --git a/hw/syn/altera/dut/Makefile b/hw/syn/altera/dut/Makefile index afd8ea1f30..746022ce38 100644 --- a/hw/syn/altera/dut/Makefile +++ b/hw/syn/altera/dut/Makefile @@ -17,58 +17,22 @@ SCRIPT_DIR := $(VORTEX_HOME)/hw/scripts IP_CACHE_DIR := $(ROOT_DIR)/hw/syn/altera/ip_cache/$(DEVICE_FAMILY) -.PHONY: unittest scope mem_unit lmem cache fpu tcu dxa core issue vortex top +# DUTS + the per-DUT definitions. Adding a DUT touches catalog.mk only. +include catalog.mk + +.PHONY: ip-gen list $(DUTS) ip-gen: $(IP_CACHE_DIR)/ip_gen.log $(IP_CACHE_DIR)/ip_gen.log: $(SCRIPT_DIR)/altera_ip_gen.sh $(IP_CACHE_DIR) -unittest: - mkdir -p unittest/$(BUILD_DIR) - cp unittest/Makefile unittest/$(BUILD_DIR) - $(MAKE) -C unittest/$(BUILD_DIR) clean && $(MAKE) -C unittest/$(BUILD_DIR) > unittest/$(BUILD_DIR)/build.log 2>&1 & - -scope: - mkdir -p scope/$(BUILD_DIR) - cp scope/Makefile scope/$(BUILD_DIR) - $(MAKE) -C scope/$(BUILD_DIR) clean && $(MAKE) -C scope/$(BUILD_DIR) > scope/$(BUILD_DIR)/build.log 2>&1 & - -mem_unit: - mkdir -p mem_unit/$(BUILD_DIR) - cp mem_unit/Makefile mem_unit/$(BUILD_DIR) - $(MAKE) -C mem_unit/$(BUILD_DIR) clean && $(MAKE) -C mem_unit/$(BUILD_DIR) > mem_unit/$(BUILD_DIR)/build.log 2>&1 & - -lmem: - mkdir -p lmem/$(BUILD_DIR) - cp lmem/Makefile lmem/$(BUILD_DIR) - $(MAKE) -C lmem/$(BUILD_DIR) clean && $(MAKE) -C lmem/$(BUILD_DIR) > lmem/$(BUILD_DIR)/build.log 2>&1 & - -cache: - mkdir -p cache/$(BUILD_DIR) - cp cache/Makefile cache/$(BUILD_DIR) - $(MAKE) -C cache/$(BUILD_DIR) clean && $(MAKE) -C cache/$(BUILD_DIR) > cache/$(BUILD_DIR)/build.log 2>&1 & - -fpu: ip-gen - mkdir -p fpu/$(BUILD_DIR) - cp fpu/Makefile fpu/$(BUILD_DIR) - $(MAKE) -C fpu/$(BUILD_DIR) clean && $(MAKE) -C fpu/$(BUILD_DIR) > fpu/$(BUILD_DIR)/build.log 2>&1 & - -core: - mkdir -p core/$(BUILD_DIR) - cp core/Makefile core/$(BUILD_DIR) - $(MAKE) -C core/$(BUILD_DIR) clean && $(MAKE) -C core/$(BUILD_DIR) > core/$(BUILD_DIR)/build.log 2>&1 & - -issue: - mkdir -p issue/$(BUILD_DIR) - cp issue/Makefile issue/$(BUILD_DIR) - $(MAKE) -C issue/$(BUILD_DIR) clean && $(MAKE) -C issue/$(BUILD_DIR) > issue/$(BUILD_DIR)/build.log 2>&1 & - -vortex: ip-gen - mkdir -p vortex/$(BUILD_DIR) - cp vortex/Makefile vortex/$(BUILD_DIR) - $(MAKE) -C vortex/$(BUILD_DIR) clean && $(MAKE) -C vortex/$(BUILD_DIR) > vortex/$(BUILD_DIR)/build.log 2>&1 & +# Each DUT builds out-of-tree in its own _/ so parallel builds +# cannot collide. build.mk is copied in as the Makefile, exactly as the per-DUT +# Makefiles used to be. +$(DUTS): + mkdir -p $(BUILD_DIR)_$@ + cp build.mk $(BUILD_DIR)_$@/Makefile + $(MAKE) -C $(BUILD_DIR)_$@ DUT=$@ clean && $(MAKE) -C $(BUILD_DIR)_$@ DUT=$@ > $(BUILD_DIR)_$@/build.log 2>&1 & -top: ip-gen - mkdir -p top/$(BUILD_DIR) - cp top/Makefile top/$(BUILD_DIR) - $(MAKE) -C top/$(BUILD_DIR) clean && $(MAKE) -C top/$(BUILD_DIR) > top/$(BUILD_DIR)/build.log 2>&1 & \ No newline at end of file +list: + @echo $(DUTS) | tr ' ' '\n' diff --git a/hw/syn/altera/dut/build.mk b/hw/syn/altera/dut/build.mk new file mode 100644 index 0000000000..d7d1fbe457 --- /dev/null +++ b/hw/syn/altera/dut/build.mk @@ -0,0 +1,27 @@ +# Per-DUT build makefile for the Altera/Quartus flow. +# +# Copied into __/ by the dispatcher and run there with +# DUT=, so this file plays the role the former dut//Makefile did. +# +# Ordering mirrors the old per-DUT Makefiles: PROJECT/CONFIGS -> common.mk +# (computes XCONFIGS, RTL_DIR, IP_CACHE_DIR, ...) -> RTL_INCLUDE/RTL_PKGS. + +ifndef DUT +$(error DUT is not set; run this through dut/Makefile, e.g. `make -C dut cache`) +endif + +include ../catalog.mk + +ifeq ($(filter $(DUT),$(DUTS)),) +$(error unknown DUT '$(DUT)'; see DUTS in dut/catalog.mk) +endif + +PROJECT = $($(DUT)_PRJ) +TOP_LEVEL_ENTITY = $(PROJECT) +SRC_FILE = $(PROJECT).sv +override CONFIGS += $($(DUT)_CFG) + +include ../common.mk + +RTL_INCLUDE = $($(DUT)_INC) +RTL_PKGS += $($(DUT)_PKG) diff --git a/hw/syn/altera/dut/cache/Makefile b/hw/syn/altera/dut/cache/Makefile deleted file mode 100755 index dafa66072b..0000000000 --- a/hw/syn/altera/dut/cache/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -PROJECT = VX_cache_top -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -include ../../common.mk - -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache -RTL_INCLUDE += -I$(UNITTEST_DIR)/cache \ No newline at end of file diff --git a/hw/syn/altera/dut/catalog.mk b/hw/syn/altera/dut/catalog.mk new file mode 100644 index 0000000000..2bca1ee61b --- /dev/null +++ b/hw/syn/altera/dut/catalog.mk @@ -0,0 +1,85 @@ +# DUT catalog for the Altera/Quartus synthesis flow. +# +# One entry per DUT, replacing the former dut//Makefile tree. Included by +# build.mk *around* common.mk: _PRJ/_CFG are needed before it (they feed +# XCONFIGS), _INC/_PKG after it (they consume XCONFIGS and the +# RTL_DIR/UNITTEST_DIR/IP_CACHE_DIR it defines), so _INC/_PKG are deferred +# (`=`, not `:=`). +# +# Add a DUT: append its name to DUTS and define its variables. No new directory, +# no dispatcher edit. + +DUTS := cache core fpu issue lmem mem_unit scope top unittest vortex + +# --------------------------------------------------------------------------- +# shared include fragments +# --------------------------------------------------------------------------- + +BASE_INC = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces + +FPU_INC = -I$(RTL_DIR)/fpu $(if $(filter -DVX_CFG_FPU_TYPE_FPNEW,$(XCONFIGS)),\ + -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/include -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src -J$(THIRD_PARTY_DIR)/cvfpu/src/fpu_div_sqrt_mvp/hdl -J$(THIRD_PARTY_DIR)/cvfpu/src) + +# TCU/DXA wiring, gated on the extension being enabled. Same content as the +# xilinx flow's; both are candidates for folding into hw/syn/extensions.mk once +# a before/after check covers it. +TCU_INC = $(if $(filter -DVX_CFG_EXT_TCU_ENABLE,$(XCONFIGS)),-I$(RTL_DIR)/tcu \ + $(if $(filter -DVX_CFG_TCU_TYPE_DPI,$(XCONFIGS)),-I$(RTL_DIR)/tcu/dpi) \ + $(if $(filter -DVX_CFG_TCU_TYPE_DSP,$(XCONFIGS)),-I$(RTL_DIR)/tcu/dsp) \ + $(if $(filter -DVX_CFG_TCU_TYPE_BHF,$(XCONFIGS)),-I$(RTL_DIR)/tcu/bhf -J$(THIRD_PARTY_DIR)/hardfloat/source/RISCV -I$(THIRD_PARTY_DIR)/hardfloat/source) \ + $(if $(filter -DVX_CFG_TCU_TYPE_FPNEW,$(XCONFIGS)),-I$(RTL_DIR)/tcu/fpnew -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/include -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src -J$(THIRD_PARTY_DIR)/cvfpu/src) \ + $(if $(filter -DVX_CFG_TCU_TYPE_TFR,$(XCONFIGS)),-I$(RTL_DIR)/tcu/tfr)) + +TCU_PKG = $(if $(filter -DVX_CFG_EXT_TCU_ENABLE,$(XCONFIGS)),$(RTL_DIR)/tcu/VX_tcu_pkg.sv \ + $(if $(filter -DVX_CFG_TCU_TYPE_FPNEW,$(XCONFIGS)),$(THIRD_PARTY_DIR)/cvfpu/src/fpnew_pkg.sv $(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src/cf_math_pkg.sv)) + +DXA_INC = $(if $(filter -DVX_CFG_EXT_DXA_ENABLE,$(XCONFIGS)),-I$(RTL_DIR)/dxa) +DXA_PKG = $(if $(filter -DVX_CFG_EXT_DXA_ENABLE,$(XCONFIGS)),$(RTL_DIR)/dxa/VX_dxa_pkg.sv) + +# --------------------------------------------------------------------------- +# per-DUT: _PRJ top module, _CFG defines, _INC includes, _PKG packages +# --------------------------------------------------------------------------- + +cache_PRJ := VX_cache_top +cache_INC = $(BASE_INC) -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache -I$(UNITTEST_DIR)/cache + +core_PRJ := VX_core_top +core_INC = $(BASE_INC) -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache \ + -I$(IP_CACHE_DIR) $(FPU_INC) $(TCU_INC) +core_PKG = $(TCU_PKG) + +# The only DUT whose top module depends on the configuration. +fpu_PRJ = $(if $(filter -DVX_CFG_FPU_TYPE_FPNEW,$(XCONFIGS)),VX_fpu_fpnew,$(if $(filter -DVX_CFG_FPU_TYPE_STD,$(XCONFIGS)),VX_fpu_std,VX_fpu_dsp)) +fpu_INC = $(BASE_INC) -I$(IP_CACHE_DIR) $(FPU_INC) + +# NOTE: the original issue/Makefile listed $(FPU_INCLUDE) twice; a duplicate -I +# is a no-op, so it is listed once here. +issue_PRJ := VX_issue_top +issue_INC = $(BASE_INC) -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm $(FPU_INC) \ + -I$(IP_CACHE_DIR) -I$(UNITTEST_DIR)/issue + +lmem_PRJ := VX_local_mem_top +lmem_INC = $(BASE_INC) -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(UNITTEST_DIR)/local_mem + +mem_unit_PRJ := VX_mem_unit_top +mem_unit_INC = $(BASE_INC) -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/core -I$(RTL_DIR)/fpu \ + -I$(UNITTEST_DIR)/mem_unit + +# The only DUT that does not want -I$(RTL_DIR)/interfaces. +scope_PRJ := VX_scope_tap +scope_INC = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs + +top_PRJ := vortex_afu +top_CFG := -DNOPAE -DPLATFORM_PROVIDES_LOCAL_MEMORY +top_INC = $(BASE_INC) -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache \ + -I$(AFU_DIR) -I$(AFU_DIR)/ccip -I$(IP_CACHE_DIR) $(FPU_INC) $(TCU_INC) $(DXA_INC) +top_PKG = $(TCU_PKG) $(DXA_PKG) + +unittest_PRJ := Unittest +unittest_INC = $(BASE_INC) -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache \ + -I$(IP_CACHE_DIR) $(FPU_INC) + +vortex_PRJ := Vortex +vortex_INC = $(BASE_INC) -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache \ + -I$(IP_CACHE_DIR) $(FPU_INC) $(TCU_INC) $(DXA_INC) +vortex_PKG = $(TCU_PKG) $(DXA_PKG) diff --git a/hw/syn/altera/dut/common.mk b/hw/syn/altera/dut/common.mk index 0dbce45ea7..962d005e46 100644 --- a/hw/syn/altera/dut/common.mk +++ b/hw/syn/altera/dut/common.mk @@ -1,4 +1,4 @@ -ROOT_DIR := $(realpath ../../../../../..) +ROOT_DIR := $(realpath ../../../../..) include $(ROOT_DIR)/config.mk # Synthesis optimization level (standardized across hw/syn): @@ -26,7 +26,10 @@ ifeq ($(DEVICE_FAMILY), arria10) DEVICE = 10AX115N3F40E2SG endif -CONFIGS += -DSYNTHESIS -DQUARTUS -DNDEBUG +# `override`: see the matching note in hw/syn/xilinx/dut/common.mk -- a +# command-line CONFIGS must add to this flow's mandatory defines, not replace +# them. +override CONFIGS += -DSYNTHESIS -DQUARTUS -DNDEBUG XCONFIGS := $(shell python3 $(ROOT_DIR)/ci/gen_config.py --config=$(VORTEX_HOME)/VX_config.toml --cflags='$(CONFIGS) -DVX_CFG_XLEN=$(XLEN)') diff --git a/hw/syn/altera/dut/core/Makefile b/hw/syn/altera/dut/core/Makefile deleted file mode 100644 index 8d6076fc1d..0000000000 --- a/hw/syn/altera/dut/core/Makefile +++ /dev/null @@ -1,39 +0,0 @@ -PROJECT = VX_core_top -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -include ../../common.mk - -#CONFIGS += -DVX_CFG_NUM_WARPS=32 -#CONFIGS += -DVX_CFG_NUM_THREADS=32 - -FPU_INCLUDE = -I$(RTL_DIR)/fpu -ifneq (,$(filter -DVX_CFG_FPU_TYPE_FPNEW, $(XCONFIGS))) - FPU_INCLUDE += -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/include -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src -J$(THIRD_PARTY_DIR)/cvfpu/src/fpu_div_sqrt_mvp/hdl -J$(THIRD_PARTY_DIR)/cvfpu/src -endif -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache -I$(IP_CACHE_DIR) $(FPU_INCLUDE) - -# Add TCU extension sources -ifneq (,$(filter -DVX_CFG_EXT_TCU_ENABLE, $(XCONFIGS))) - RTL_PKGS += $(RTL_DIR)/tcu/VX_tcu_pkg.sv - RTL_INCLUDE += -I$(RTL_DIR)/tcu - ifneq (,$(filter -DVX_CFG_TCU_TYPE_DPI, $(XCONFIGS))) - RTL_INCLUDE += -I$(RTL_DIR)/tcu/dpi - endif - ifneq (,$(filter -DVX_CFG_TCU_TYPE_DSP, $(XCONFIGS))) - RTL_INCLUDE += -I$(RTL_DIR)/tcu/dsp - endif - ifneq (,$(filter -DVX_CFG_TCU_TYPE_BHF, $(XCONFIGS))) - RTL_INCLUDE += -I$(RTL_DIR)/tcu/bhf - RTL_INCLUDE += -J$(THIRD_PARTY_DIR)/hardfloat/source/RISCV - RTL_INCLUDE += -I$(THIRD_PARTY_DIR)/hardfloat/source - endif - ifneq (,$(filter -DVX_CFG_TCU_TYPE_FPNEW, $(XCONFIGS))) - RTL_PKGS += $(THIRD_PARTY_DIR)/cvfpu/src/fpnew_pkg.sv $(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src/cf_math_pkg.sv - RTL_INCLUDE += -I$(RTL_DIR)/tcu/fpnew - RTL_INCLUDE += -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/include -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src -J$(THIRD_PARTY_DIR)/cvfpu/src - endif - ifneq (,$(filter -DVX_CFG_TCU_TYPE_TFR, $(XCONFIGS))) - RTL_INCLUDE += -I$(RTL_DIR)/tcu/tfr - endif -endif diff --git a/hw/syn/altera/dut/fpu/Makefile b/hw/syn/altera/dut/fpu/Makefile deleted file mode 100644 index 794f3b3ac0..0000000000 --- a/hw/syn/altera/dut/fpu/Makefile +++ /dev/null @@ -1,19 +0,0 @@ -include ../../common.mk - -PROJECT = VX_fpu_dsp - -ifneq (,$(filter -DVX_CFG_FPU_TYPE_FPNEW, $(XCONFIGS))) -PROJECT = VX_fpu_fpnew -endif -ifneq (,$(filter -DVX_CFG_FPU_TYPE_STD, $(XCONFIGS))) -PROJECT = VX_fpu_std -endif - -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -FPU_INCLUDE = -I$(RTL_DIR)/fpu -ifneq (,$(filter -DVX_CFG_FPU_TYPE_FPNEW, $(XCONFIGS))) - FPU_INCLUDE += -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/include -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src -J$(THIRD_PARTY_DIR)/cvfpu/src/fpu_div_sqrt_mvp/hdl -J$(THIRD_PARTY_DIR)/cvfpu/src -endif -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -I$(IP_CACHE_DIR) $(FPU_INCLUDE) diff --git a/hw/syn/altera/dut/issue/Makefile b/hw/syn/altera/dut/issue/Makefile deleted file mode 100644 index 6199c29808..0000000000 --- a/hw/syn/altera/dut/issue/Makefile +++ /dev/null @@ -1,15 +0,0 @@ -PROJECT = VX_issue_top -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -include ../../common.mk - -#CONFIGS += -DVX_CFG_NUM_WARPS=32 -#CONFIGS += -DVX_CFG_NUM_THREADS=32 - -FPU_INCLUDE = -I$(RTL_DIR)/fpu -ifneq (,$(filter -DVX_CFG_FPU_TYPE_FPNEW, $(XCONFIGS))) - FPU_INCLUDE += -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/include -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src -J$(THIRD_PARTY_DIR)/cvfpu/src/fpu_div_sqrt_mvp/hdl -J$(THIRD_PARTY_DIR)/cvfpu/src -endif -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm $(FPU_INCLUDE) -I$(IP_CACHE_DIR) $(FPU_INCLUDE) -RTL_INCLUDE += -I$(UNITTEST_DIR)/issue \ No newline at end of file diff --git a/hw/syn/altera/dut/lmem/Makefile b/hw/syn/altera/dut/lmem/Makefile deleted file mode 100755 index a5566d8bd5..0000000000 --- a/hw/syn/altera/dut/lmem/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -PROJECT = VX_local_mem_top -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -include ../../common.mk - -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -RTL_INCLUDE += -I$(UNITTEST_DIR)/local_mem \ No newline at end of file diff --git a/hw/syn/altera/dut/mem_unit/Makefile b/hw/syn/altera/dut/mem_unit/Makefile deleted file mode 100755 index 011c1e8bd4..0000000000 --- a/hw/syn/altera/dut/mem_unit/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -PROJECT = VX_mem_unit_top -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -include ../../common.mk - -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/core -I$(RTL_DIR)/fpu -RTL_INCLUDE += -I$(UNITTEST_DIR)/mem_unit \ No newline at end of file diff --git a/hw/syn/altera/dut/scope/Makefile b/hw/syn/altera/dut/scope/Makefile deleted file mode 100755 index a2297ac5d3..0000000000 --- a/hw/syn/altera/dut/scope/Makefile +++ /dev/null @@ -1,7 +0,0 @@ -PROJECT = VX_scope_tap -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -include ../../common.mk - -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs diff --git a/hw/syn/altera/dut/top/Makefile b/hw/syn/altera/dut/top/Makefile deleted file mode 100644 index ed9373368d..0000000000 --- a/hw/syn/altera/dut/top/Makefile +++ /dev/null @@ -1,51 +0,0 @@ -PROJECT = vortex_afu -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -include ../../common.mk - -# AFU parameters -CONFIGS += -DNOPAE -CONFIGS += -DPLATFORM_PROVIDES_LOCAL_MEMORY - -#CONFIGS += -DVX_CFG_NUM_CORES=2 -#CONFIGS += -DVX_CFG_NUM_WARPS=32 -#CONFIGS += -DVX_CFG_NUM_THREADS=32 -#CONFIGS += -DVX_CFG_L2_ENABLE - -FPU_INCLUDE = -I$(RTL_DIR)/fpu -ifneq (,$(filter -DVX_CFG_FPU_TYPE_FPNEW, $(XCONFIGS))) - FPU_INCLUDE += -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/include -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src -J$(THIRD_PARTY_DIR)/cvfpu/src/fpu_div_sqrt_mvp/hdl -J$(THIRD_PARTY_DIR)/cvfpu/src -endif -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache -I$(AFU_DIR) -I$(AFU_DIR)/ccip -I$(IP_CACHE_DIR) $(FPU_INCLUDE) - -# Add TCU extension sources -ifneq (,$(filter -DVX_CFG_EXT_TCU_ENABLE, $(XCONFIGS))) - RTL_PKGS += $(RTL_DIR)/tcu/VX_tcu_pkg.sv - RTL_INCLUDE += -I$(RTL_DIR)/tcu - ifneq (,$(filter -DVX_CFG_TCU_TYPE_DPI, $(XCONFIGS))) - RTL_INCLUDE += -I$(RTL_DIR)/tcu/dpi - endif - ifneq (,$(filter -DVX_CFG_TCU_TYPE_DSP, $(XCONFIGS))) - RTL_INCLUDE += -I$(RTL_DIR)/tcu/dsp - endif - ifneq (,$(filter -DVX_CFG_TCU_TYPE_BHF, $(XCONFIGS))) - RTL_INCLUDE += -I$(RTL_DIR)/tcu/bhf - RTL_INCLUDE += -J$(THIRD_PARTY_DIR)/hardfloat/source/RISCV - RTL_INCLUDE += -I$(THIRD_PARTY_DIR)/hardfloat/source - endif - ifneq (,$(filter -DVX_CFG_TCU_TYPE_FPNEW, $(XCONFIGS))) - RTL_PKGS += $(THIRD_PARTY_DIR)/cvfpu/src/fpnew_pkg.sv $(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src/cf_math_pkg.sv - RTL_INCLUDE += -I$(RTL_DIR)/tcu/fpnew - RTL_INCLUDE += -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/include -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src -J$(THIRD_PARTY_DIR)/cvfpu/src - endif - ifneq (,$(filter -DVX_CFG_TCU_TYPE_TFR, $(XCONFIGS))) - RTL_INCLUDE += -I$(RTL_DIR)/tcu/tfr - endif -endif - -# Add DXA extension sources -ifneq (,$(filter -DVX_CFG_EXT_DXA_ENABLE, $(XCONFIGS))) - RTL_PKGS += $(RTL_DIR)/dxa/VX_dxa_pkg.sv - RTL_INCLUDE += -I$(RTL_DIR)/dxa -endif diff --git a/hw/syn/altera/dut/unittest/Makefile b/hw/syn/altera/dut/unittest/Makefile deleted file mode 100644 index 8ce6ff52d3..0000000000 --- a/hw/syn/altera/dut/unittest/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -PROJECT = Unittest -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -include ../../common.mk - -FPU_INCLUDE = -I$(RTL_DIR)/fpu -ifneq (,$(filter -DVX_CFG_FPU_TYPE_FPNEW, $(XCONFIGS))) - FPU_INCLUDE += -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/include -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src -J$(THIRD_PARTY_DIR)/cvfpu/src/fpu_div_sqrt_mvp/hdl -J$(THIRD_PARTY_DIR)/cvfpu/src -endif -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache -I$(IP_CACHE_DIR) $(FPU_INCLUDE) diff --git a/hw/syn/altera/dut/vortex/Makefile b/hw/syn/altera/dut/vortex/Makefile deleted file mode 100644 index 93ef176775..0000000000 --- a/hw/syn/altera/dut/vortex/Makefile +++ /dev/null @@ -1,47 +0,0 @@ -PROJECT = Vortex -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -include ../../common.mk - -#CONFIGS += -DVX_CFG_NUM_CORES=2 -#CONFIGS += -DVX_CFG_NUM_WARPS=32 -#CONFIGS += -DVX_CFG_NUM_THREADS=32 -#CONFIGS += -DVX_CFG_L2_ENABLE - -FPU_INCLUDE = -I$(RTL_DIR)/fpu -ifneq (,$(filter -DVX_CFG_FPU_TYPE_FPNEW, $(XCONFIGS))) - FPU_INCLUDE += -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/include -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src -J$(THIRD_PARTY_DIR)/cvfpu/src/fpu_div_sqrt_mvp/hdl -J$(THIRD_PARTY_DIR)/cvfpu/src -endif -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache -I$(IP_CACHE_DIR) $(FPU_INCLUDE) - -# Add TCU extension sources -ifneq (,$(filter -DVX_CFG_EXT_TCU_ENABLE, $(XCONFIGS))) - RTL_PKGS += $(RTL_DIR)/tcu/VX_tcu_pkg.sv - RTL_INCLUDE += -I$(RTL_DIR)/tcu - ifneq (,$(filter -DVX_CFG_TCU_TYPE_DPI, $(XCONFIGS))) - RTL_INCLUDE += -I$(RTL_DIR)/tcu/dpi - endif - ifneq (,$(filter -DVX_CFG_TCU_TYPE_DSP, $(XCONFIGS))) - RTL_INCLUDE += -I$(RTL_DIR)/tcu/dsp - endif - ifneq (,$(filter -DVX_CFG_TCU_TYPE_BHF, $(XCONFIGS))) - RTL_INCLUDE += -I$(RTL_DIR)/tcu/bhf - RTL_INCLUDE += -J$(THIRD_PARTY_DIR)/hardfloat/source/RISCV - RTL_INCLUDE += -I$(THIRD_PARTY_DIR)/hardfloat/source - endif - ifneq (,$(filter -DVX_CFG_TCU_TYPE_FPNEW, $(XCONFIGS))) - RTL_PKGS += $(THIRD_PARTY_DIR)/cvfpu/src/fpnew_pkg.sv $(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src/cf_math_pkg.sv - RTL_INCLUDE += -I$(RTL_DIR)/tcu/fpnew - RTL_INCLUDE += -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/include -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src -J$(THIRD_PARTY_DIR)/cvfpu/src - endif - ifneq (,$(filter -DVX_CFG_TCU_TYPE_TFR, $(XCONFIGS))) - RTL_INCLUDE += -I$(RTL_DIR)/tcu/tfr - endif -endif - -# Add DXA extension sources -ifneq (,$(filter -DVX_CFG_EXT_DXA_ENABLE, $(XCONFIGS))) - RTL_PKGS += $(RTL_DIR)/dxa/VX_dxa_pkg.sv - RTL_INCLUDE += -I$(RTL_DIR)/dxa -endif diff --git a/hw/syn/xilinx/dut/Makefile b/hw/syn/xilinx/dut/Makefile index f02f20fa77..802263a1ad 100644 --- a/hw/syn/xilinx/dut/Makefile +++ b/hw/syn/xilinx/dut/Makefile @@ -3,8 +3,6 @@ include $(ROOT_DIR)/config.mk PREFIX ?= build -BUILD_DIR := $(PREFIX) - SAIF_FILE ?= SAIF_INST ?= @@ -13,118 +11,26 @@ SAIF_INST ?= OPT_LEVEL ?= 3 export OPT_LEVEL -.PHONY: unittest scope mem_unit lmem cache vm tensor gfx fpu tcu dxa tex raster om rtu rtu_top cp core issue vortex top - -unittest: - mkdir -p unittest/$(BUILD_DIR) - cp unittest/Makefile unittest/$(BUILD_DIR) - $(MAKE) -C unittest/$(BUILD_DIR) clean && $(MAKE) -C unittest/$(BUILD_DIR) > unittest/$(BUILD_DIR)/build.log 2>&1 & - -scope: - mkdir -p scope/$(BUILD_DIR) - cp scope/Makefile scope/$(BUILD_DIR) - $(MAKE) -C scope/$(BUILD_DIR) clean && $(MAKE) -C scope/$(BUILD_DIR) > scope/$(BUILD_DIR)/build.log 2>&1 & - -mem_unit: - mkdir -p mem_unit/$(BUILD_DIR) - cp mem_unit/Makefile mem_unit/$(BUILD_DIR) - $(MAKE) -C mem_unit/$(BUILD_DIR) clean && $(MAKE) -C mem_unit/$(BUILD_DIR) > mem_unit/$(BUILD_DIR)/build.log 2>&1 & - -lmem: - mkdir -p lmem/$(BUILD_DIR) - cp lmem/Makefile lmem/$(BUILD_DIR) - $(MAKE) -C lmem/$(BUILD_DIR) clean && $(MAKE) -C lmem/$(BUILD_DIR) > lmem/$(BUILD_DIR)/build.log 2>&1 & - -cache: - mkdir -p cache/$(BUILD_DIR) - cp cache/Makefile cache/$(BUILD_DIR) - $(MAKE) -C cache/$(BUILD_DIR) clean && $(MAKE) -C cache/$(BUILD_DIR) > cache/$(BUILD_DIR)/build.log 2>&1 & - -vm: - mkdir -p vm/$(BUILD_DIR) - cp vm/Makefile vm/$(BUILD_DIR) - $(MAKE) -C vm/$(BUILD_DIR) clean && $(MAKE) -C vm/$(BUILD_DIR) > vm/$(BUILD_DIR)/build.log 2>&1 & - -tensor: - mkdir -p tensor/$(BUILD_DIR) - cp tensor/Makefile tensor/$(BUILD_DIR) - $(MAKE) -C tensor/$(BUILD_DIR) clean && $(MAKE) -C tensor/$(BUILD_DIR) > tensor/$(BUILD_DIR)/build.log 2>&1 & - -gfx: - mkdir -p gfx/$(BUILD_DIR) - cp gfx/Makefile gfx/$(BUILD_DIR) - $(MAKE) -C gfx/$(BUILD_DIR) clean && $(MAKE) -C gfx/$(BUILD_DIR) > gfx/$(BUILD_DIR)/build.log 2>&1 & - -fpu: - mkdir -p fpu/$(BUILD_DIR) - cp fpu/Makefile fpu/$(BUILD_DIR) - $(MAKE) -C fpu/$(BUILD_DIR) clean && $(MAKE) -C fpu/$(BUILD_DIR) > fpu/$(BUILD_DIR)/build.log 2>&1 & - -tcu: - mkdir -p tcu/$(BUILD_DIR) - cp tcu/Makefile tcu/$(BUILD_DIR) - $(MAKE) -C tcu/$(BUILD_DIR) clean && $(MAKE) -C tcu/$(BUILD_DIR) > tcu/$(BUILD_DIR)/build.log 2>&1 & +# DUTS + the per-DUT definitions. Adding a DUT touches catalog.mk only. +include catalog.mk -core: - mkdir -p core/$(BUILD_DIR) - cp core/Makefile core/$(BUILD_DIR) - $(MAKE) -C core/$(BUILD_DIR) clean && $(MAKE) -C core/$(BUILD_DIR) > core/$(BUILD_DIR)/build.log 2>&1 & - -issue: - mkdir -p issue/$(BUILD_DIR) - cp issue/Makefile issue/$(BUILD_DIR) - $(MAKE) -C issue/$(BUILD_DIR) clean && $(MAKE) -C issue/$(BUILD_DIR) > issue/$(BUILD_DIR)/build.log 2>&1 & - -vortex: - mkdir -p vortex/$(BUILD_DIR) - cp vortex/Makefile vortex/$(BUILD_DIR) - $(MAKE) -C vortex/$(BUILD_DIR) clean && $(MAKE) -C vortex/$(BUILD_DIR) > vortex/$(BUILD_DIR)/build.log 2>&1 & - -top: - mkdir -p top/$(BUILD_DIR) - cp top/Makefile top/$(BUILD_DIR) - $(MAKE) -C top/$(BUILD_DIR) clean && $(MAKE) -C top/$(BUILD_DIR) > top/$(BUILD_DIR)/build.log 2>&1 & - -dxa: - mkdir -p dxa/$(BUILD_DIR) - cp dxa/Makefile dxa/$(BUILD_DIR) - $(MAKE) -C dxa/$(BUILD_DIR) clean && $(MAKE) -C dxa/$(BUILD_DIR) > dxa/$(BUILD_DIR)/build.log 2>&1 & - -cp: - mkdir -p cp/$(BUILD_DIR) - cp cp/Makefile cp/$(BUILD_DIR) - $(MAKE) -C cp/$(BUILD_DIR) clean && $(MAKE) -C cp/$(BUILD_DIR) > cp/$(BUILD_DIR)/build.log 2>&1 & - -tex: - mkdir -p tex/$(BUILD_DIR) - cp tex/Makefile tex/$(BUILD_DIR) - $(MAKE) -C tex/$(BUILD_DIR) clean && $(MAKE) -C tex/$(BUILD_DIR) > tex/$(BUILD_DIR)/build.log 2>&1 & - -raster: - mkdir -p raster/$(BUILD_DIR) - cp raster/Makefile raster/$(BUILD_DIR) - $(MAKE) -C raster/$(BUILD_DIR) clean && $(MAKE) -C raster/$(BUILD_DIR) > raster/$(BUILD_DIR)/build.log 2>&1 & - -om: - mkdir -p om/$(BUILD_DIR) - cp om/Makefile om/$(BUILD_DIR) - $(MAKE) -C om/$(BUILD_DIR) clean && $(MAKE) -C om/$(BUILD_DIR) > om/$(BUILD_DIR)/build.log 2>&1 & - -rtu: - mkdir -p rtu/$(BUILD_DIR) - cp rtu/Makefile rtu/$(BUILD_DIR) - $(MAKE) -C rtu/$(BUILD_DIR) clean && $(MAKE) -C rtu/$(BUILD_DIR) > rtu/$(BUILD_DIR)/build.log 2>&1 & - -rtu_top: - mkdir -p rtu_top/$(BUILD_DIR) - cp rtu_top/Makefile rtu_top/$(BUILD_DIR) - $(MAKE) -C rtu_top/$(BUILD_DIR) clean && $(MAKE) -C rtu_top/$(BUILD_DIR) > rtu_top/$(BUILD_DIR)/build.log 2>&1 & +# Each DUT builds out-of-tree in its own _/ so parallel builds (and +# a hand-run synthesis on the same machine) cannot collide. build.mk is copied +# in as the Makefile, exactly as the per-DUT Makefiles used to be. +.PHONY: $(DUTS) +$(DUTS): + mkdir -p $(PREFIX)_$@ + cp build.mk $(PREFIX)_$@/Makefile + $(MAKE) -C $(PREFIX)_$@ DUT=$@ clean && $(MAKE) -C $(PREFIX)_$@ DUT=$@ > $(PREFIX)_$@/build.log 2>&1 & # Re-run power analysis on an existing build without rebuilding the design. # Usage: make -power SAIF_FILE= [SAIF_INST=] %-power: - @target=$*; \ - if [ ! -d $$target/$(BUILD_DIR) ]; then \ - echo "ERROR: $$target/$(BUILD_DIR) not found. Run 'make $$target' first."; exit 1; \ + @if [ ! -d $(PREFIX)_$* ]; then \ + echo "ERROR: $(PREFIX)_$* not found. Run 'make $*' first."; exit 1; \ fi - $(MAKE) -C $*/$(BUILD_DIR) power SAIF_FILE=$(SAIF_FILE) SAIF_INST=$(SAIF_INST) \ No newline at end of file + $(MAKE) -C $(PREFIX)_$* DUT=$* power SAIF_FILE=$(SAIF_FILE) SAIF_INST=$(SAIF_INST) + +.PHONY: list +list: + @echo $(DUTS) | tr ' ' '\n' diff --git a/hw/syn/xilinx/dut/build.mk b/hw/syn/xilinx/dut/build.mk new file mode 100644 index 0000000000..878af652d0 --- /dev/null +++ b/hw/syn/xilinx/dut/build.mk @@ -0,0 +1,34 @@ +# Per-DUT build makefile for the Xilinx flow. +# +# Copied into _/ by the dispatcher and run there with DUT=, +# so this file plays the role the former dut//Makefile did. Everything it +# needs comes from catalog.mk; nothing here is DUT-specific. +# +# Ordering matters and mirrors the old per-DUT Makefiles exactly: +# PROJECT/CONFIGS -> common.mk (computes XCONFIGS, RTL_DIR, ...) -> +# RTL_INCLUDE/RTL_PKGS (which read XCONFIGS) -> extensions.mk + +ifndef DUT +$(error DUT is not set; run this through dut/Makefile, e.g. `make -C dut tcu`) +endif + +include ../catalog.mk + +ifeq ($(filter $(DUT),$(DUTS)),) +$(error unknown DUT '$(DUT)'; see DUTS in dut/catalog.mk) +endif + +PROJECT = $($(DUT)_PRJ) +TOP_LEVEL_ENTITY = $(PROJECT) +SRC_FILE = $(PROJECT).sv +FPU_IP = $($(DUT)_IP) +override CONFIGS += $($(DUT)_CFG) + +include ../common.mk + +RTL_INCLUDE = $($(DUT)_INC) +RTL_PKGS += $($(DUT)_PKG) + +ifeq ($($(DUT)_EXT),1) +include $(VORTEX_HOME)/hw/syn/extensions.mk +endif diff --git a/hw/syn/xilinx/dut/cache/Makefile b/hw/syn/xilinx/dut/cache/Makefile deleted file mode 100644 index dafa66072b..0000000000 --- a/hw/syn/xilinx/dut/cache/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -PROJECT = VX_cache_top -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -include ../../common.mk - -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache -RTL_INCLUDE += -I$(UNITTEST_DIR)/cache \ No newline at end of file diff --git a/hw/syn/xilinx/dut/catalog.mk b/hw/syn/xilinx/dut/catalog.mk new file mode 100644 index 0000000000..1bb5edaa98 --- /dev/null +++ b/hw/syn/xilinx/dut/catalog.mk @@ -0,0 +1,151 @@ +# DUT catalog for the Xilinx synthesis flow. +# +# One entry per DUT, replacing the former dut//Makefile tree. Included by +# build.mk *around* common.mk: the _CFG/_PRJ/_IP values are needed before it +# (they feed XCONFIGS), the _INC/_PKG values after it (they consume XCONFIGS and +# the RTL_DIR/UNITTEST_DIR/THIRD_PARTY_DIR it defines). The _INC/_PKG variables +# are therefore deferred (`=`, not `:=`) so their $(filter …$(XCONFIGS)) tests +# evaluate at use, not at parse. +# +# Add a DUT: append its name to DUTS and define the five variables. No new +# directory, no dispatcher edit. + +DUTS := cache core cp dxa fpu gfx issue lmem mem_unit om raster rtu \ + scope tcu tensor tex top unittest vm vortex + +# --------------------------------------------------------------------------- +# shared include fragments +# --------------------------------------------------------------------------- + +BASE_INC = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces + +# cvfpu's headers are -J (library) paths, added only for the FPNEW backend. +FPU_INC = -I$(RTL_DIR)/fpu $(if $(filter -DVX_CFG_FPU_TYPE_FPNEW,$(XCONFIGS)),\ + -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/include -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src -J$(THIRD_PARTY_DIR)/cvfpu/src/fpu_div_sqrt_mvp/hdl -J$(THIRD_PARTY_DIR)/cvfpu/src) + +# TCU backend selection. Kept here rather than deferred to extensions.mk so the +# flatten is semantics-preserving; folding these into extensions.mk is a +# separate change with its own before/after check. +TCU_INC = -I$(RTL_DIR)/tcu \ + $(if $(filter -DVX_CFG_TCU_TYPE_DPI,$(XCONFIGS)),-I$(RTL_DIR)/tcu/dpi) \ + $(if $(filter -DVX_CFG_TCU_TYPE_DSP,$(XCONFIGS)),-I$(RTL_DIR)/tcu/dsp) \ + $(if $(filter -DVX_CFG_TCU_TYPE_BHF,$(XCONFIGS)),-I$(RTL_DIR)/tcu/bhf -J$(THIRD_PARTY_DIR)/hardfloat/source/RISCV -I$(THIRD_PARTY_DIR)/hardfloat/source) \ + $(if $(filter -DVX_CFG_TCU_TYPE_FPNEW,$(XCONFIGS)),-I$(RTL_DIR)/tcu/fpnew -I$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/include -I$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src -I$(THIRD_PARTY_DIR)/cvfpu/src) \ + $(if $(filter -DVX_CFG_TCU_TYPE_TFR,$(XCONFIGS)),-I$(RTL_DIR)/tcu/tfr) + +TCU_PKG = $(if $(filter -DVX_CFG_TCU_TYPE_FPNEW,$(XCONFIGS)),\ + $(THIRD_PARTY_DIR)/cvfpu/src/fpnew_pkg.sv $(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src/cf_math_pkg.sv) + +# --------------------------------------------------------------------------- +# per-DUT: _PRJ top module, _IP FPU IP, _CFG defines, _INC includes, _PKG pkgs, +# _EXT=1 to include extensions.mk +# --------------------------------------------------------------------------- + +cache_PRJ := VX_cache_top +cache_INC = $(BASE_INC) -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache -I$(UNITTEST_DIR)/cache + +core_PRJ := VX_core_top +core_IP := 1 +core_EXT := 1 +core_INC = $(BASE_INC) -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache $(FPU_INC) -I$(UNITTEST_DIR)/core + +cp_PRJ := VX_cp_core_top +cp_INC = $(BASE_INC) -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache -I$(RTL_DIR)/fpu -I$(RTL_DIR)/cp -I$(UNITTEST_DIR)/cp_core + +dxa_PRJ := VX_dxa_core_top +dxa_CFG := -DVX_CFG_EXT_DXA_ENABLE +dxa_INC = $(BASE_INC) -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/dxa -I$(UNITTEST_DIR)/dxa_core +dxa_PKG = $(RTL_DIR)/dxa/VX_dxa_pkg.sv + +# The only DUT whose top module depends on the configuration. +fpu_IP := 1 +fpu_PRJ = $(if $(filter -DVX_CFG_FPU_TYPE_FPNEW,$(XCONFIGS)),VX_fpu_fpnew,$(if $(filter -DVX_CFG_FPU_TYPE_STD,$(XCONFIGS)),VX_fpu_std,VX_fpu_dsp)) +fpu_INC = $(BASE_INC) $(FPU_INC) + +gfx_PRJ := VX_gfx_top +gfx_IP := 1 +gfx_CFG := -DVX_CFG_EXT_TEX_ENABLE -DVX_CFG_EXT_RTU_ENABLE -DVX_CFG_EXT_RASTER_ENABLE \ + -DVX_CFG_EXT_OM_ENABLE -DVX_CFG_EXT_DXA_ENABLE -DVX_CFG_RASTER_EARLYZ_ENABLE \ + -DVX_CFG_NUM_THREADS=16 -DVX_CFG_NUM_WARPS=16 -DVX_CFG_NUM_CORES=2 \ + -DVX_CFG_SOCKET_SIZE=2 -DVX_CFG_PLATFORM_MEMORY_NUM_BANKS=1 \ + -DVX_CFG_L2_ENABLE -DVX_CFG_L2_SIZE=262144 +gfx_INC = $(BASE_INC) -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/cache -I$(RTL_DIR)/vm -I$(RTL_DIR)/fpu \ + -I$(RTL_DIR)/tex -I$(RTL_DIR)/rtu -I$(RTL_DIR)/raster -I$(RTL_DIR)/om -I$(RTL_DIR)/dxa \ + -I$(UNITTEST_DIR)/gfx +gfx_PKG = $(RTL_DIR)/fpu/VX_fpu_pkg.sv $(RTL_DIR)/dxa/VX_dxa_pkg.sv $(RTL_DIR)/tex/VX_tex_pkg.sv \ + $(RTL_DIR)/rtu/VX_rtu_pkg.sv $(RTL_DIR)/raster/VX_raster_pkg.sv $(RTL_DIR)/om/VX_om_pkg.sv + +# NOTE: the original issue/Makefile listed $(FPU_INCLUDE) twice; a duplicate -I +# is a no-op, so it is listed once here. +issue_PRJ := VX_issue_top +issue_INC = $(BASE_INC) -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm $(FPU_INC) -I$(UNITTEST_DIR)/issue + +lmem_PRJ := VX_local_mem_top +lmem_INC = $(BASE_INC) -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache -I$(UNITTEST_DIR)/local_mem + +mem_unit_PRJ := VX_mem_unit_top +mem_unit_INC = $(BASE_INC) -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/core -I$(RTL_DIR)/fpu -I$(UNITTEST_DIR)/mem_unit + +om_PRJ := VX_om_core_top +om_CFG := -DVX_CFG_EXT_OM_ENABLE +om_INC = $(BASE_INC) -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/om -I$(UNITTEST_DIR)/om_core +om_PKG = $(RTL_DIR)/om/VX_om_pkg.sv + +raster_PRJ := VX_raster_core_top +raster_CFG := -DVX_CFG_EXT_RASTER_ENABLE +raster_INC = $(BASE_INC) -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/raster -I$(UNITTEST_DIR)/raster_core +raster_PKG = $(RTL_DIR)/raster/VX_raster_pkg.sv + +rtu_PRJ := VX_rtu_core_top +rtu_IP := 1 +rtu_CFG := -DVX_CFG_EXT_RTU_ENABLE +rtu_INC = $(BASE_INC) -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/fpu -I$(RTL_DIR)/rtu -I$(UNITTEST_DIR)/rtu_core +rtu_PKG = $(RTL_DIR)/fpu/VX_fpu_pkg.sv $(RTL_DIR)/rtu/VX_rtu_pkg.sv + +# The only DUT that does not want -I$(RTL_DIR)/interfaces. +scope_PRJ := VX_scope_tap +scope_INC = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs + +tcu_PRJ := VX_tcu_unit_top +tcu_IP := 1 +tcu_CFG := -DVX_CFG_EXT_TCU_ENABLE -DVX_CFG_EXT_F_DISABLE +tcu_INC = $(BASE_INC) -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(UNITTEST_DIR)/tcu_unit $(TCU_INC) +tcu_PKG = $(TCU_PKG) + +tensor_PRJ := VX_tensor_top +tensor_IP := 1 +tensor_CFG := -DVX_CFG_EXT_TCU_ENABLE -DVX_CFG_EXT_DXA_ENABLE -DVX_CFG_TCU_WGMMA_ENABLE \ + -DVX_CFG_TCU_MX_ENABLE -DVX_CFG_EXT_A_ENABLE \ + -DVX_CFG_NUM_THREADS=16 -DVX_CFG_NUM_WARPS=16 -DVX_CFG_NUM_CORES=2 \ + -DVX_CFG_SOCKET_SIZE=2 -DVX_CFG_PLATFORM_MEMORY_NUM_BANKS=1 \ + -DVX_CFG_L2_ENABLE -DVX_CFG_L2_SIZE=262144 +tensor_INC = $(BASE_INC) -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/cache -I$(RTL_DIR)/vm \ + -I$(RTL_DIR)/fpu -I$(RTL_DIR)/dxa -I$(UNITTEST_DIR)/tensor $(TCU_INC) +tensor_PKG = $(TCU_PKG) $(RTL_DIR)/tcu/VX_tcu_pkg.sv $(RTL_DIR)/dxa/VX_dxa_pkg.sv + +tex_PRJ := VX_tex_core_top +tex_CFG := -DVX_CFG_EXT_TEX_ENABLE +tex_INC = $(BASE_INC) -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/tex -I$(UNITTEST_DIR)/tex_core +tex_PKG = $(RTL_DIR)/tex/VX_tex_pkg.sv + +top_PRJ := VX_afu_wrap +top_IP := 1 +top_EXT := 1 +top_INC = $(BASE_INC) -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache \ + -I$(RTL_DIR)/cp -I$(AFU_DIR) -I$(AFU_COMMON_DIR) $(FPU_INC) + +unittest_PRJ := Unittest +unittest_INC = $(BASE_INC) -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache $(FPU_INC) + +vm_PRJ := VX_vm_top +vm_CFG := -DVX_CFG_VM_ENABLE -DVX_CFG_EXT_A_ENABLE -DVX_CFG_NUM_CORES=2 \ + -DVX_CFG_NUM_THREADS=16 -DVX_CFG_NUM_WARPS=16 \ + -DVX_CFG_PLATFORM_MEMORY_NUM_BANKS=1 -DVX_CFG_L2_ENABLE \ + -DVX_CFG_L2_SIZE=262144 -DVX_CFG_DCACHE_LATENCY=3 +vm_INC = $(BASE_INC) -I$(RTL_DIR)/mem -I$(RTL_DIR)/cache -I$(RTL_DIR)/vm -I$(UNITTEST_DIR)/vm +vm_PKG = $(RTL_DIR)/vm/VX_tlb_pkg.sv + +vortex_PRJ := Vortex +vortex_IP := 1 +vortex_EXT := 1 +vortex_INC = $(BASE_INC) -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache $(FPU_INC) diff --git a/hw/syn/xilinx/dut/common.mk b/hw/syn/xilinx/dut/common.mk index 0e086b3ea7..e76070a987 100644 --- a/hw/syn/xilinx/dut/common.mk +++ b/hw/syn/xilinx/dut/common.mk @@ -1,4 +1,4 @@ -ROOT_DIR := $(realpath ../../../../../..) +ROOT_DIR := $(realpath ../../../../..) include $(ROOT_DIR)/config.mk # Exports VERILATOR/VERILATOR_PATH as absolute paths. gen_sources.sh resolves # verilator from the environment, so without this the flow silently depends on @@ -30,7 +30,12 @@ UNITTEST_DIR := $(VORTEX_HOME)/hw/unittest NCPUS := $(shell lscpu | grep "^Core(s) per socket:" | awk '{print $$4}') JOBS ?= $(shell echo $$(( $(NCPUS) > $(MAX_JOBS) ? $(MAX_JOBS) : $(NCPUS) ))) -CONFIGS += -DSYNTHESIS -DVIVADO -DNDEBUG +# `override`: these are facts about this flow, not preferences. A caller that +# sets CONFIGS on the MAKE COMMAND LINE would otherwise kill every `CONFIGS +=` +# here and in build.mk silently -- command-line variables win over `+=`, and +# nothing warns. With `override`, a command-line CONFIGS is extra defines on +# top, which is what it reads as. +override CONFIGS += -DSYNTHESIS -DVIVADO -DNDEBUG XCONFIGS := $(shell python3 $(ROOT_DIR)/ci/gen_config.py --config=$(VORTEX_HOME)/VX_config.toml --cflags='$(CONFIGS) -DVX_CFG_XLEN=$(XLEN)') diff --git a/hw/syn/xilinx/dut/core/Makefile b/hw/syn/xilinx/dut/core/Makefile deleted file mode 100644 index 45a5a48d6b..0000000000 --- a/hw/syn/xilinx/dut/core/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -PROJECT = VX_core_top -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv -FPU_IP = 1 - -include ../../common.mk - -#CONFIGS += -DVX_CFG_NUM_WARPS=32 -#CONFIGS += -DVX_CFG_NUM_THREADS=32 - -FPU_INCLUDE = -I$(RTL_DIR)/fpu -ifneq (,$(filter -DVX_CFG_FPU_TYPE_FPNEW, $(XCONFIGS))) - FPU_INCLUDE += -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/include -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src -J$(THIRD_PARTY_DIR)/cvfpu/src/fpu_div_sqrt_mvp/hdl -J$(THIRD_PARTY_DIR)/cvfpu/src -endif -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache $(FPU_INCLUDE) -RTL_INCLUDE += -I$(UNITTEST_DIR)/core - -# Per-extension RTL sources (TCU/DXA/V/RTU/TEX/RASTER/OM) — -# single source of truth, appended based on the resolved XCONFIGS. -include $(VORTEX_HOME)/hw/syn/extensions.mk diff --git a/hw/syn/xilinx/dut/cp/Makefile b/hw/syn/xilinx/dut/cp/Makefile deleted file mode 100644 index c661956f03..0000000000 --- a/hw/syn/xilinx/dut/cp/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -PROJECT = VX_cp_core_top -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -include ../../common.mk - -# VX_cp_core_top (hw/unittest/cp_core) wraps the Command Processor core. -# Its package/interfaces live in hw/rtl/cp; the wrapper itself in unittest/cp_core. -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -RTL_INCLUDE += -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache -I$(RTL_DIR)/fpu -RTL_INCLUDE += -I$(RTL_DIR)/cp -RTL_INCLUDE += -I$(UNITTEST_DIR)/cp_core diff --git a/hw/syn/xilinx/dut/dxa/Makefile b/hw/syn/xilinx/dut/dxa/Makefile deleted file mode 100644 index 8ffdf0a558..0000000000 --- a/hw/syn/xilinx/dut/dxa/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -PROJECT = VX_dxa_core_top -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -CONFIGS += -DVX_CFG_EXT_DXA_ENABLE - -include ../../common.mk - -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -RTL_INCLUDE += -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/dxa -RTL_INCLUDE += -I$(UNITTEST_DIR)/dxa_core - -RTL_PKGS += $(RTL_DIR)/dxa/VX_dxa_pkg.sv - diff --git a/hw/syn/xilinx/dut/fpu/Makefile b/hw/syn/xilinx/dut/fpu/Makefile deleted file mode 100644 index 2d712275af..0000000000 --- a/hw/syn/xilinx/dut/fpu/Makefile +++ /dev/null @@ -1,23 +0,0 @@ -# On Xilinx, VX_fma_unit/VX_fdiv_unit/VX_fsqrt_unit select the vendor FP operator IP -# (xil_fma/fdiv/sqrt) for the F32 flush-to-zero path, so the IP must be generated. -FPU_IP = 1 - -include ../../common.mk - -PROJECT = VX_fpu_dsp - -ifneq (,$(filter -DVX_CFG_FPU_TYPE_FPNEW, $(XCONFIGS))) -PROJECT = VX_fpu_fpnew -endif -ifneq (,$(filter -DVX_CFG_FPU_TYPE_STD, $(XCONFIGS))) -PROJECT = VX_fpu_std -endif - -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -FPU_INCLUDE = -I$(RTL_DIR)/fpu -ifneq (,$(filter -DVX_CFG_FPU_TYPE_FPNEW, $(XCONFIGS))) - FPU_INCLUDE += -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/include -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src -J$(THIRD_PARTY_DIR)/cvfpu/src/fpu_div_sqrt_mvp/hdl -J$(THIRD_PARTY_DIR)/cvfpu/src -endif -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces $(FPU_INCLUDE) diff --git a/hw/syn/xilinx/dut/gfx/Makefile b/hw/syn/xilinx/dut/gfx/Makefile deleted file mode 100644 index 496f64e2a1..0000000000 --- a/hw/syn/xilinx/dut/gfx/Makefile +++ /dev/null @@ -1,41 +0,0 @@ -PROJECT = VX_gfx_top -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv -FPU_IP = 1 - -# Graphics sandbox default configuration: -# - 1 socket x 2 cores (real tex/rtu socket arbiters) -# - all four engines on; early-Z on (raster <-> ocache coherence path) -# - DXA enabled so the socket memory arbiter carries its full production -# input set {icache, dcache, tcache, rtcache, DXA gmem} -# - default engine counts and cache geometry (tcache 8K, rtcache 8K, -# rcache 4K, ocache 16K) -# - 1x L2 cache, 256KB (drop -DVX_CFG_L2_ENABLE => PASSTHRU wires) -CONFIGS += -DVX_CFG_EXT_TEX_ENABLE -CONFIGS += -DVX_CFG_EXT_RTU_ENABLE -CONFIGS += -DVX_CFG_EXT_RASTER_ENABLE -CONFIGS += -DVX_CFG_EXT_OM_ENABLE -CONFIGS += -DVX_CFG_EXT_DXA_ENABLE -CONFIGS += -DVX_CFG_RASTER_EARLYZ_ENABLE -CONFIGS += -DVX_CFG_NUM_THREADS=16 -CONFIGS += -DVX_CFG_NUM_WARPS=16 -CONFIGS += -DVX_CFG_NUM_CORES=2 -CONFIGS += -DVX_CFG_SOCKET_SIZE=2 -CONFIGS += -DVX_CFG_PLATFORM_MEMORY_NUM_BANKS=1 -CONFIGS += -DVX_CFG_L2_ENABLE -CONFIGS += -DVX_CFG_L2_SIZE=262144 - -include ../../common.mk - -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -RTL_INCLUDE += -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/cache -I$(RTL_DIR)/vm -I$(RTL_DIR)/fpu -RTL_INCLUDE += -I$(RTL_DIR)/tex -I$(RTL_DIR)/rtu -I$(RTL_DIR)/raster -I$(RTL_DIR)/om -RTL_INCLUDE += -I$(RTL_DIR)/dxa -RTL_INCLUDE += -I$(UNITTEST_DIR)/gfx - -RTL_PKGS += $(RTL_DIR)/fpu/VX_fpu_pkg.sv -RTL_PKGS += $(RTL_DIR)/dxa/VX_dxa_pkg.sv -RTL_PKGS += $(RTL_DIR)/tex/VX_tex_pkg.sv -RTL_PKGS += $(RTL_DIR)/rtu/VX_rtu_pkg.sv -RTL_PKGS += $(RTL_DIR)/raster/VX_raster_pkg.sv -RTL_PKGS += $(RTL_DIR)/om/VX_om_pkg.sv diff --git a/hw/syn/xilinx/dut/issue/Makefile b/hw/syn/xilinx/dut/issue/Makefile deleted file mode 100644 index e12f18210a..0000000000 --- a/hw/syn/xilinx/dut/issue/Makefile +++ /dev/null @@ -1,15 +0,0 @@ -PROJECT = VX_issue_top -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -include ../../common.mk - -#CONFIGS += -DVX_CFG_NUM_WARPS=32 -#CONFIGS += -DVX_CFG_NUM_THREADS=32 - -FPU_INCLUDE = -I$(RTL_DIR)/fpu -ifneq (,$(filter -DVX_CFG_FPU_TYPE_FPNEW, $(XCONFIGS))) - FPU_INCLUDE += -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/include -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src -J$(THIRD_PARTY_DIR)/cvfpu/src/fpu_div_sqrt_mvp/hdl -J$(THIRD_PARTY_DIR)/cvfpu/src -endif -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm $(FPU_INCLUDE) $(FPU_INCLUDE) -RTL_INCLUDE += -I$(UNITTEST_DIR)/issue \ No newline at end of file diff --git a/hw/syn/xilinx/dut/lmem/Makefile b/hw/syn/xilinx/dut/lmem/Makefile deleted file mode 100644 index 65fdb64cfe..0000000000 --- a/hw/syn/xilinx/dut/lmem/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -PROJECT = VX_local_mem_top -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -include ../../common.mk - -# hw/rtl/cache is on the path for VX_amo_alu: the LMEM banks perform atomics -# with the same combinational ALU the cache uses rather than a second copy. -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache -RTL_INCLUDE += -I$(UNITTEST_DIR)/local_mem \ No newline at end of file diff --git a/hw/syn/xilinx/dut/mem_unit/Makefile b/hw/syn/xilinx/dut/mem_unit/Makefile deleted file mode 100644 index 011c1e8bd4..0000000000 --- a/hw/syn/xilinx/dut/mem_unit/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -PROJECT = VX_mem_unit_top -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -include ../../common.mk - -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/core -I$(RTL_DIR)/fpu -RTL_INCLUDE += -I$(UNITTEST_DIR)/mem_unit \ No newline at end of file diff --git a/hw/syn/xilinx/dut/om/Makefile b/hw/syn/xilinx/dut/om/Makefile deleted file mode 100644 index 97b1359eca..0000000000 --- a/hw/syn/xilinx/dut/om/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -PROJECT = VX_om_core_top -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -CONFIGS += -DVX_CFG_EXT_OM_ENABLE - -include ../../common.mk - -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -RTL_INCLUDE += -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/om -RTL_INCLUDE += -I$(UNITTEST_DIR)/om_core - -RTL_PKGS += $(RTL_DIR)/om/VX_om_pkg.sv diff --git a/hw/syn/xilinx/dut/raster/Makefile b/hw/syn/xilinx/dut/raster/Makefile deleted file mode 100644 index bdca22d985..0000000000 --- a/hw/syn/xilinx/dut/raster/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -PROJECT = VX_raster_core_top -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -CONFIGS += -DVX_CFG_EXT_RASTER_ENABLE - -include ../../common.mk - -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -RTL_INCLUDE += -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/raster -RTL_INCLUDE += -I$(UNITTEST_DIR)/raster_core - -RTL_PKGS += $(RTL_DIR)/raster/VX_raster_pkg.sv diff --git a/hw/syn/xilinx/dut/rtu/Makefile b/hw/syn/xilinx/dut/rtu/Makefile deleted file mode 100644 index 504e734cd5..0000000000 --- a/hw/syn/xilinx/dut/rtu/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -PROJECT = VX_rtu_core_top -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv -FPU_IP = 1 - -CONFIGS += -DVX_CFG_EXT_RTU_ENABLE - -include ../../common.mk - -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -RTL_INCLUDE += -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/fpu -I$(RTL_DIR)/rtu -RTL_INCLUDE += -I$(UNITTEST_DIR)/rtu_core - -RTL_PKGS += $(RTL_DIR)/fpu/VX_fpu_pkg.sv $(RTL_DIR)/rtu/VX_rtu_pkg.sv diff --git a/hw/syn/xilinx/dut/rtu_top/Makefile b/hw/syn/xilinx/dut/rtu_top/Makefile deleted file mode 100644 index f84c12e770..0000000000 --- a/hw/syn/xilinx/dut/rtu_top/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -PROJECT = VX_rtu_top -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -CONFIGS += -DVX_CFG_EXT_RTU_ENABLE - -include ../../common.mk - -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -RTL_INCLUDE += -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/fpu -I$(RTL_DIR)/rtu -RTL_INCLUDE += -I$(UNITTEST_DIR)/rtu_top - -RTL_PKGS += $(RTL_DIR)/fpu/VX_fpu_pkg.sv $(RTL_DIR)/rtu/VX_rtu_pkg.sv diff --git a/hw/syn/xilinx/dut/scope/Makefile b/hw/syn/xilinx/dut/scope/Makefile deleted file mode 100644 index a2297ac5d3..0000000000 --- a/hw/syn/xilinx/dut/scope/Makefile +++ /dev/null @@ -1,7 +0,0 @@ -PROJECT = VX_scope_tap -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -include ../../common.mk - -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs diff --git a/hw/syn/xilinx/dut/tcu/Makefile b/hw/syn/xilinx/dut/tcu/Makefile deleted file mode 100644 index d54030fb28..0000000000 --- a/hw/syn/xilinx/dut/tcu/Makefile +++ /dev/null @@ -1,33 +0,0 @@ -PROJECT = VX_tcu_unit_top -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv -FPU_IP = 1 - -CONFIGS += -DVX_CFG_EXT_TCU_ENABLE -DVX_CFG_EXT_F_DISABLE - -include ../../common.mk - -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -RTL_INCLUDE += -I$(UNITTEST_DIR)/tcu_unit - -# Add TCU extension sources -RTL_INCLUDE += -I$(RTL_DIR)/tcu -ifneq (,$(filter -DVX_CFG_TCU_TYPE_DPI, $(XCONFIGS))) -RTL_INCLUDE += -I$(RTL_DIR)/tcu/dpi -endif -ifneq (,$(filter -DVX_CFG_TCU_TYPE_DSP, $(XCONFIGS))) -RTL_INCLUDE += -I$(RTL_DIR)/tcu/dsp -endif -ifneq (,$(filter -DVX_CFG_TCU_TYPE_BHF, $(XCONFIGS))) -RTL_INCLUDE += -I$(RTL_DIR)/tcu/bhf -RTL_INCLUDE += -J$(THIRD_PARTY_DIR)/hardfloat/source/RISCV -RTL_INCLUDE += -I$(THIRD_PARTY_DIR)/hardfloat/source -endif -ifneq (,$(filter -DVX_CFG_TCU_TYPE_FPNEW, $(XCONFIGS))) -RTL_INCLUDE += -I$(RTL_DIR)/tcu/fpnew -RTL_PKGS += $(THIRD_PARTY_DIR)/cvfpu/src/fpnew_pkg.sv $(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src/cf_math_pkg.sv -RTL_INCLUDE += -I$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/include -I$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src -I$(THIRD_PARTY_DIR)/cvfpu/src -endif -ifneq (,$(filter -DVX_CFG_TCU_TYPE_TFR, $(XCONFIGS))) -RTL_INCLUDE += -I$(RTL_DIR)/tcu/tfr -endif diff --git a/hw/syn/xilinx/dut/tensor/Makefile b/hw/syn/xilinx/dut/tensor/Makefile deleted file mode 100644 index 0fd7acaaeb..0000000000 --- a/hw/syn/xilinx/dut/tensor/Makefile +++ /dev/null @@ -1,56 +0,0 @@ -PROJECT = VX_tensor_top -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv -FPU_IP = 1 - -# Tensor sandbox default configuration: -# - 1 socket x 2 cores (real DXA request arb + staging-return switch) -# - TCU: default type, FP16 + WGMMA + MX => all three TCU memory paths -# (WGMMA LMEM prefetch, TCU_LD metadata loads, ordinary LSU stream) -# - LMEM enabled, banks = LSU lanes; dcache with AMO -# - 1x L2 cache, 256KB, fed by {dcache mem ports, DXA gmem} via the -# socket memory arbiter (drop -DVX_CFG_L2_ENABLE => PASSTHRU wires) -CONFIGS += -DVX_CFG_EXT_TCU_ENABLE -CONFIGS += -DVX_CFG_EXT_DXA_ENABLE -CONFIGS += -DVX_CFG_TCU_WGMMA_ENABLE -CONFIGS += -DVX_CFG_TCU_MX_ENABLE -CONFIGS += -DVX_CFG_EXT_A_ENABLE -CONFIGS += -DVX_CFG_NUM_THREADS=16 -CONFIGS += -DVX_CFG_NUM_WARPS=16 -CONFIGS += -DVX_CFG_NUM_CORES=2 -CONFIGS += -DVX_CFG_SOCKET_SIZE=2 -CONFIGS += -DVX_CFG_PLATFORM_MEMORY_NUM_BANKS=1 -CONFIGS += -DVX_CFG_L2_ENABLE -CONFIGS += -DVX_CFG_L2_SIZE=262144 - -include ../../common.mk - -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -RTL_INCLUDE += -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/cache -I$(RTL_DIR)/vm -RTL_INCLUDE += -I$(RTL_DIR)/fpu -I$(RTL_DIR)/dxa -RTL_INCLUDE += -I$(UNITTEST_DIR)/tensor - -# Add TCU extension sources -RTL_INCLUDE += -I$(RTL_DIR)/tcu -ifneq (,$(filter -DVX_CFG_TCU_TYPE_DPI, $(XCONFIGS))) -RTL_INCLUDE += -I$(RTL_DIR)/tcu/dpi -endif -ifneq (,$(filter -DVX_CFG_TCU_TYPE_DSP, $(XCONFIGS))) -RTL_INCLUDE += -I$(RTL_DIR)/tcu/dsp -endif -ifneq (,$(filter -DVX_CFG_TCU_TYPE_BHF, $(XCONFIGS))) -RTL_INCLUDE += -I$(RTL_DIR)/tcu/bhf -RTL_INCLUDE += -J$(THIRD_PARTY_DIR)/hardfloat/source/RISCV -RTL_INCLUDE += -I$(THIRD_PARTY_DIR)/hardfloat/source -endif -ifneq (,$(filter -DVX_CFG_TCU_TYPE_FPNEW, $(XCONFIGS))) -RTL_INCLUDE += -I$(RTL_DIR)/tcu/fpnew -RTL_PKGS += $(THIRD_PARTY_DIR)/cvfpu/src/fpnew_pkg.sv $(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src/cf_math_pkg.sv -RTL_INCLUDE += -I$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/include -I$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src -I$(THIRD_PARTY_DIR)/cvfpu/src -endif -ifneq (,$(filter -DVX_CFG_TCU_TYPE_TFR, $(XCONFIGS))) -RTL_INCLUDE += -I$(RTL_DIR)/tcu/tfr -endif - -RTL_PKGS += $(RTL_DIR)/tcu/VX_tcu_pkg.sv -RTL_PKGS += $(RTL_DIR)/dxa/VX_dxa_pkg.sv diff --git a/hw/syn/xilinx/dut/tex/Makefile b/hw/syn/xilinx/dut/tex/Makefile deleted file mode 100644 index b86b0fa713..0000000000 --- a/hw/syn/xilinx/dut/tex/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -PROJECT = VX_tex_core_top -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -CONFIGS += -DVX_CFG_EXT_TEX_ENABLE - -include ../../common.mk - -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -RTL_INCLUDE += -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/tex -RTL_INCLUDE += -I$(UNITTEST_DIR)/tex_core - -RTL_PKGS += $(RTL_DIR)/tex/VX_tex_pkg.sv diff --git a/hw/syn/xilinx/dut/top/Makefile b/hw/syn/xilinx/dut/top/Makefile deleted file mode 100644 index d4dea5527f..0000000000 --- a/hw/syn/xilinx/dut/top/Makefile +++ /dev/null @@ -1,23 +0,0 @@ -PROJECT = VX_afu_wrap -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv -FPU_IP = 1 - -include ../../common.mk - -#CONFIGS += -DVX_CFG_NUM_CORES=2 -#CONFIGS += -DVX_CFG_NUM_WARPS=32 -#CONFIGS += -DVX_CFG_NUM_THREADS=32 -#CONFIGS += -DVX_CFG_L2_ENABLE - -FPU_INCLUDE = -I$(RTL_DIR)/fpu -ifneq (,$(filter -DVX_CFG_FPU_TYPE_FPNEW, $(XCONFIGS))) - FPU_INCLUDE += -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/include -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src -J$(THIRD_PARTY_DIR)/cvfpu/src/fpu_div_sqrt_mvp/hdl -J$(THIRD_PARTY_DIR)/cvfpu/src -endif -# VX_afu_wrap instantiates the Command Processor; its package/interfaces -# (VX_cp_pkg, VX_cp_axi_m_if, VX_cp_gpu_if, ...) are auto-discovered via -I. -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache -I$(RTL_DIR)/cp -I$(AFU_DIR) -I$(AFU_COMMON_DIR) $(FPU_INCLUDE) - -# Per-extension RTL sources (TCU/DXA/V/RTU/TEX/RASTER/OM) — -# single source of truth, appended based on the resolved XCONFIGS. -include $(VORTEX_HOME)/hw/syn/extensions.mk diff --git a/hw/syn/xilinx/dut/unittest/Makefile b/hw/syn/xilinx/dut/unittest/Makefile deleted file mode 100644 index 4179a540c6..0000000000 --- a/hw/syn/xilinx/dut/unittest/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -PROJECT = Unittest -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -include ../../common.mk - -FPU_INCLUDE = -I$(RTL_DIR)/fpu -ifneq (,$(filter -DVX_CFG_FPU_TYPE_FPNEW, $(XCONFIGS))) - FPU_INCLUDE += -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/include -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src -J$(THIRD_PARTY_DIR)/cvfpu/src/fpu_div_sqrt_mvp/hdl -J$(THIRD_PARTY_DIR)/cvfpu/src -endif -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache $(FPU_INCLUDE) \ No newline at end of file diff --git a/hw/syn/xilinx/dut/vm/Makefile b/hw/syn/xilinx/dut/vm/Makefile deleted file mode 100644 index 956d547c56..0000000000 --- a/hw/syn/xilinx/dut/vm/Makefile +++ /dev/null @@ -1,31 +0,0 @@ -PROJECT = VX_vm_top -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv - -# MMU-network sandbox default configuration: -# - 2 cores => 2 sockets, each 4x 64B parallel memory requests -# (NUM_THREADS=16 => DCACHE_NUM_REQS=4, 64B line) -# - AMO enabled -# - per socket: 2x L1 MMU (dTLB+iTLB) + 2x L1 cache (dcache+icache, 16KB, 4 lanes) -# - 1x L2 cache, 256KB, fed by {2 socket L1 ports, PTW} => 3 lanes -# (PLATFORM_MEMORY_NUM_BANKS=1 => L1_MEM_PORTS=1 => L2_NUM_REQS=3) -# - shared L2 TLB (L2_TLB_SIZE=512) + 1 PTW with 2 walkers -CONFIGS += -DVX_CFG_VM_ENABLE -CONFIGS += -DVX_CFG_EXT_A_ENABLE -CONFIGS += -DVX_CFG_NUM_CORES=2 -CONFIGS += -DVX_CFG_NUM_THREADS=16 -CONFIGS += -DVX_CFG_NUM_WARPS=16 -CONFIGS += -DVX_CFG_PLATFORM_MEMORY_NUM_BANKS=1 -CONFIGS += -DVX_CFG_L2_ENABLE -CONFIGS += -DVX_CFG_L2_SIZE=262144 -# L1 hit latency 3 (PIPE_EX=1): registers the way-select so the tag->data -# write path is broken. Matches the multicore integration DUT (top). -CONFIGS += -DVX_CFG_DCACHE_LATENCY=3 - -include ../../common.mk - -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -RTL_INCLUDE += -I$(RTL_DIR)/mem -I$(RTL_DIR)/cache -I$(RTL_DIR)/vm -RTL_INCLUDE += -I$(UNITTEST_DIR)/vm - -RTL_PKGS += $(RTL_DIR)/vm/VX_tlb_pkg.sv diff --git a/hw/syn/xilinx/dut/vortex/Makefile b/hw/syn/xilinx/dut/vortex/Makefile deleted file mode 100644 index 71f9aecb40..0000000000 --- a/hw/syn/xilinx/dut/vortex/Makefile +++ /dev/null @@ -1,21 +0,0 @@ -PROJECT = Vortex -TOP_LEVEL_ENTITY = $(PROJECT) -SRC_FILE = $(PROJECT).sv -FPU_IP = 1 - -include ../../common.mk - -#CONFIGS += -DVX_CFG_NUM_CORES=2 -#CONFIGS += -DVX_CFG_NUM_WARPS=32 -#CONFIGS += -DVX_CFG_NUM_THREADS=32 -#CONFIGS += -DVX_CFG_L2_ENABLE - -FPU_INCLUDE = -I$(RTL_DIR)/fpu -ifneq (,$(filter -DVX_CFG_FPU_TYPE_FPNEW, $(XCONFIGS))) - FPU_INCLUDE += -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/include -J$(THIRD_PARTY_DIR)/cvfpu/src/common_cells/src -J$(THIRD_PARTY_DIR)/cvfpu/src/fpu_div_sqrt_mvp/hdl -J$(THIRD_PARTY_DIR)/cvfpu/src -endif -RTL_INCLUDE = -I$(ROOT_DIR)/sw -I$(ROOT_DIR)/hw -I$(RTL_DIR) -I$(RTL_DIR)/libs -I$(RTL_DIR)/interfaces -I$(RTL_DIR)/core -I$(RTL_DIR)/mem -I$(RTL_DIR)/vm -I$(RTL_DIR)/cache $(FPU_INCLUDE) - -# Per-extension RTL sources (TCU/DXA/V/RTU/TEX/RASTER/OM) — -# single source of truth, appended based on the resolved XCONFIGS. -include $(VORTEX_HOME)/hw/syn/extensions.mk diff --git a/hw/unittest/rtu_top/VX_rtu_top.sv b/hw/unittest/rtu_top/VX_rtu_top.sv deleted file mode 100644 index 44dba483e4..0000000000 --- a/hw/unittest/rtu_top/VX_rtu_top.sv +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright © 2019-2023 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -`include "VX_define.vh" - -// Flat-port wrapper around the full per-core RTU: VX_rtu_unit (SFU shim + -// ray-state register file) wired to VX_rtu_core (traversal engine) over the -// RTU bus. Exposes the SFU execute/result interfaces and the RTCache port as -// flat logic ports for Verilator and FPGA/ASIC synthesis. The SFU execute and -// result payloads are carried as opaque packed buses sized from sfu_execute_t / -// sfu_result_t. - -module VX_rtu_top import VX_gpu_pkg::*, VX_rtu_pkg::*; #( - parameter `STRING INSTANCE_ID = "", - parameter NUM_LANES = `VX_CFG_NUM_SFU_LANES, - parameter TAG_WIDTH = RTU_REQ_TAG_WIDTH, - // RTCache port geometry (line-granular: word size == cache line) - parameter CACHE_WORD_SIZE = RTCACHE_WORD_SIZE, - parameter CACHE_TAG_WIDTH = RTCACHE_TAG_WIDTH, - parameter CACHE_ADDR_WIDTH = `VX_CFG_MEM_ADDR_WIDTH - `CLOG2(CACHE_WORD_SIZE), - parameter EXEC_BITS = $bits(sfu_execute_t), - parameter RES_BITS = $bits(sfu_result_t) -) ( - input wire clk, - input wire reset, - - // ----------------------------------------------------------------------- - // SFU execute interface (slave): one ray-tracing op per dispatch - // ----------------------------------------------------------------------- - input wire execute_valid, - input wire [EXEC_BITS-1:0] execute_data, - output wire execute_ready, - - // SFU result interface (master) - output wire result_valid, - output wire [RES_BITS-1:0] result_data, - input wire result_ready, - - // ----------------------------------------------------------------------- - // RTCache port (master): node/leaf line fetch - // ----------------------------------------------------------------------- - output wire cache_req_valid, - output wire cache_req_rw, - output wire [CACHE_WORD_SIZE-1:0] cache_req_byteen, - output wire [CACHE_ADDR_WIDTH-1:0] cache_req_addr, - output wire [CACHE_WORD_SIZE*8-1:0] cache_req_data, - output wire [CACHE_TAG_WIDTH-1:0] cache_req_tag, - input wire cache_req_ready, - - input wire cache_rsp_valid, - input wire [CACHE_WORD_SIZE*8-1:0] cache_rsp_data, - input wire [CACHE_TAG_WIDTH-1:0] cache_rsp_tag, - output wire cache_rsp_ready -); - // ----------------------------------------------------------------------- - // SFU execute / result interfaces - // ----------------------------------------------------------------------- - VX_execute_if #(.data_t (sfu_execute_t)) execute_if (); - assign execute_if.valid = execute_valid; - assign execute_if.data = execute_data; - assign execute_ready = execute_if.ready; - - VX_result_if #(.data_t (sfu_result_t)) result_if (); - assign result_valid = result_if.valid; - assign result_data = result_if.data; - assign result_if.ready = result_ready; - - // ----------------------------------------------------------------------- - // RTU bus (unit master → core slave) - // ----------------------------------------------------------------------- - VX_rtu_bus_if #( - .NUM_LANES (NUM_LANES), - .TAG_WIDTH (TAG_WIDTH) - ) rtu_bus_if (); - - // ----------------------------------------------------------------------- - // RTCache memory port - // ----------------------------------------------------------------------- - VX_mem_bus_if #( - .DATA_SIZE (CACHE_WORD_SIZE), - .TAG_WIDTH (CACHE_TAG_WIDTH) - ) cache_bus_if (); - - assign cache_req_valid = cache_bus_if.req_valid; - assign cache_req_rw = cache_bus_if.req_data.rw; - assign cache_req_byteen = cache_bus_if.req_data.byteen; - assign cache_req_addr = cache_bus_if.req_data.addr; - assign cache_req_data = cache_bus_if.req_data.data; - assign cache_req_tag = cache_bus_if.req_data.tag; - assign cache_bus_if.req_ready = cache_req_ready; - `UNUSED_VAR (cache_bus_if.req_data.attr) - - assign cache_bus_if.rsp_valid = cache_rsp_valid; - assign cache_bus_if.rsp_data.data = cache_rsp_data; - assign cache_bus_if.rsp_data.tag = cache_rsp_tag; - assign cache_rsp_ready = cache_bus_if.rsp_ready; - - // ----------------------------------------------------------------------- - // DUT: per-core SFU shim + cluster traversal engine - // ----------------------------------------------------------------------- - VX_rtu_unit #( - .INSTANCE_ID (INSTANCE_ID), - .CORE_ID (0), - .NUM_LANES (NUM_LANES) - ) rtu_unit ( - .clk (clk), - .reset (reset), - .execute_if (execute_if), - .result_if (result_if), - .rtu_bus_if (rtu_bus_if) - ); - - VX_rtu_core #( - .INSTANCE_ID (INSTANCE_ID), - .NUM_LANES (NUM_LANES), - .TAG_WIDTH (TAG_WIDTH) - ) rtu_core ( - .clk (clk), - .reset (reset), - .rtu_bus_if (rtu_bus_if), - .cache_bus_if (cache_bus_if) - ); - -endmodule From 9c35f2595c26344e2a72f6dd54a58d51564ebb91 Mon Sep 17 00:00:00 2001 From: Blaise Tine Date: Tue, 1 Sep 2026 17:47:40 -0700 Subject: [PATCH 06/10] hw/syn/libs: move the ASAP7 collateral out of ci/ Four asap7_* files sat in ci/ because that is where the installer was written, not because they belong to CI: they are standard-cell collateral, and hw/syn/libs/ is where the rest of it lives. configure copies only Makefile, *.mk and *.in out of a hw* directory, so the manifest and preparation script are not in the build tree. They are inputs rather than build artifacts, so install.sh.in reads them from the source tree through @VORTEX_HOME@ instead of being copied; only the installer itself is instantiated. Co-Authored-By: Claude Opus 5 --- ci/asap7.LICENSE => hw/syn/libs/asap7/LICENSE | 0 ci/asap7_install.sh.in => hw/syn/libs/asap7/install.sh.in | 8 +++++--- ci/asap7_manifest.txt => hw/syn/libs/asap7/manifest.txt | 0 ci/asap7_prepare.py => hw/syn/libs/asap7/prepare.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) rename ci/asap7.LICENSE => hw/syn/libs/asap7/LICENSE (100%) rename ci/asap7_install.sh.in => hw/syn/libs/asap7/install.sh.in (91%) rename ci/asap7_manifest.txt => hw/syn/libs/asap7/manifest.txt (100%) rename ci/asap7_prepare.py => hw/syn/libs/asap7/prepare.py (98%) diff --git a/ci/asap7.LICENSE b/hw/syn/libs/asap7/LICENSE similarity index 100% rename from ci/asap7.LICENSE rename to hw/syn/libs/asap7/LICENSE diff --git a/ci/asap7_install.sh.in b/hw/syn/libs/asap7/install.sh.in similarity index 91% rename from ci/asap7_install.sh.in rename to hw/syn/libs/asap7/install.sh.in index 8bda8b144f..821aad3748 100644 --- a/ci/asap7_install.sh.in +++ b/hw/syn/libs/asap7/install.sh.in @@ -1,9 +1,11 @@ #!/bin/bash set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -MANIFEST="$SCRIPT_DIR/asap7_manifest.txt" -PREPARE="$SCRIPT_DIR/asap7_prepare.py" +# Read the manifest and preparation script from the source tree: they are +# inputs, not build artifacts, so they are never copied into the build dir +# (configure instantiates only this .in file). +MANIFEST="@VORTEX_HOME@/hw/syn/libs/asap7/manifest.txt" +PREPARE="@VORTEX_HOME@/hw/syn/libs/asap7/prepare.py" usage() { echo "Usage: $0 --install-dir= [--vt=lvt|rvt] [--models]" >&2 diff --git a/ci/asap7_manifest.txt b/hw/syn/libs/asap7/manifest.txt similarity index 100% rename from ci/asap7_manifest.txt rename to hw/syn/libs/asap7/manifest.txt diff --git a/ci/asap7_prepare.py b/hw/syn/libs/asap7/prepare.py similarity index 98% rename from ci/asap7_prepare.py rename to hw/syn/libs/asap7/prepare.py index 1fde4c589c..17625d0286 100644 --- a/ci/asap7_prepare.py +++ b/hw/syn/libs/asap7/prepare.py @@ -38,7 +38,7 @@ def verify(downloads, files): for expected, name in files: path = downloads / name if not path.is_file(): - raise SystemExit(f'ERROR: missing {path}; rerun asap7_install.sh') + raise SystemExit(f'ERROR: missing {path}; rerun install.sh') actual = sha256(path) if actual != expected: raise SystemExit(f'ERROR: checksum mismatch for {path}') From 17c5e842d278053a97ab305217965a72b17f4cc5 Mon Sep 17 00:00:00 2001 From: Blaise Tine Date: Tue, 1 Sep 2026 17:47:54 -0700 Subject: [PATCH 07/10] hw/syn/yosys: per-DUT synthesis, a machine-readable summary, and two fixes The flow could only synthesize whatever TOP_LEVEL_ENTITY it was handed. dut/catalog.mk now declares the twelve DUTs -- the same hw/unittest/*/VX_*_top.sv wrappers the Xilinx flow synthesizes -- as a top, an include path and a define set each, and dut/Makefile forwards them to the flow. No per-DUT Makefiles are needed: the flow already includes hw/syn/extensions.mk, which derives each extension's RTL_PKGS and include paths from the resolved XCONFIGS. PREFIX defaults to the DUT name so each gets its own tree. That is load-bearing, not hygienic: $(BUILD_DIR)/src is cached and is NOT regenerated when EXTRA_INCLUDE changes, so two DUTs sharing a tree silently synthesize the first one's sources. synth_summary.py collapses reports/{stat_lib,sram_area,worst_slack,tns, power}.rpt into one synth_summary.csv, mirroring what the Xilinx flow's project.tcl already writes. Format knowledge stays next to the tool that produces it, so a consumer needs no parser per report. Two measurement bugs, both of which made a number look right while being wrong: - report_wns is worst NEGATIVE slack and clamps at zero, so every design that closed reported 0.00 and an Fmax derived from it was the target clock read back. run_sta.tcl now also emits report_worst_slack, which is signed: om at 800 MHz closes with +0.0318 ns, i.e. 820.9 MHz. ABC maps to the target period and stops, so closing with picoseconds of margin is the normal outcome and the sign is the whole signal -- 2 digits of precision is not enough to see it. - A CONFIGS on a make command line silently defeats every `CONFIGS +=` in a makefile. dut/Makefile passed each DUT's defines that way, which killed the flow's own `CONFIGS += -DSYNTHESIS -DASIC -DYOSYS`, so the ASIC RAM path was never taken and arrays inferred to flip-flops: the cache DUT came out at 1.08M cells, 122,724 um^2 and zero SRAM area against an actual 423k / 47,068 / 16,307. The dispatcher now uses the flow's documented EXTRA_CONFIGS channel and the mandatory defines are `override`, so the trap cannot be re-armed. Co-Authored-By: Claude Opus 5 --- hw/syn/yosys/Makefile | 21 ++++-- hw/syn/yosys/dut/Makefile | 41 +++++++++++ hw/syn/yosys/dut/catalog.mk | 73 ++++++++++++++++++++ hw/syn/yosys/run_sta.tcl | 20 ++++-- hw/syn/yosys/synth_summary.py | 125 ++++++++++++++++++++++++++++++++++ 5 files changed, 272 insertions(+), 8 deletions(-) create mode 100644 hw/syn/yosys/dut/Makefile create mode 100644 hw/syn/yosys/dut/catalog.mk create mode 100755 hw/syn/yosys/synth_summary.py diff --git a/hw/syn/yosys/Makefile b/hw/syn/yosys/Makefile index 13b60daccd..d184de1902 100644 --- a/hw/syn/yosys/Makefile +++ b/hw/syn/yosys/Makefile @@ -2,7 +2,6 @@ ROOT_DIR := $(realpath ../../..) include $(ROOT_DIR)/config.mk include $(ROOT_DIR)/hw/syn/common.mk # SV2V_PATH/YOSYS_PATH/STA_PATH + exported SV2V/YOSYS/STA -CLOCK_FREQ ?= 800 DELAY_UNC ?= 0.02 DELAY_IO ?= 0.05 ABC_DRIVER_CELL ?= @@ -31,12 +30,13 @@ endif SRC_DIR := $(VORTEX_HOME)/hw/syn/yosys LIB_DIR := $(VORTEX_HOME)/hw/syn/libs ASAP7_DIR := $(ROOT_DIR)/hw/syn/libs/asap7 -ASAP7_INSTALL := $(ROOT_DIR)/ci/asap7_install.sh +ASAP7_INSTALL := $(ASAP7_DIR)/install.sh ASAP7_LIB := $(ASAP7_DIR)/lib/asap7_$(ASAP7_VT)_$(CORNER).lib ASAP7_CELL_MODELS := $(ASAP7_DIR)/verilog/asap7_$(ASAP7_VT)_cells.v ifeq ($(PDK),asap7) DEFAULT_LIB := $(ASAP7_LIB) +DEFAULT_CLOCK_FREQ := 800 ifeq ($(strip $(ABC_DRIVER_CELL)),) ABC_DRIVER_CELL := BUFx4_ASAP7_75t_R endif @@ -45,10 +45,15 @@ ABC_LOAD := 5.0 endif else ifeq ($(PDK),nangate45) DEFAULT_LIB := $(LIB_DIR)/NangateOpenCellLibrary_typical.lib +DEFAULT_CLOCK_FREQ := 800 else $(error Unsupported PDK=$(PDK); expected asap7 or nangate45) endif +# Target clock, defaulted per PDK above. It must bind for Fmax to mean anything: +# a design that closes with slack reports its target back, not its capability. +CLOCK_FREQ ?= $(DEFAULT_CLOCK_FREQ) + ifneq ($(strip $(LIB_TGT)),) SYN_LIB_ENV := LIB_TGT=$(LIB_TGT) else ifneq ($(strip $(LIB_ROOT)),) @@ -113,11 +118,17 @@ CONFIGS_64c := -DVX_CFG_NUM_CLUSTERS=4 -DVX_CFG_NUM_CORES=16 -DVX_CFG_L2_ENABLE CONFIGS += $(CONFIGS_$(NUM_CORES)c) endif -CONFIGS += -DSYNTHESIS -DASIC -DYOSYS +# `override`, because these are facts about this flow, not preferences: without +# ASIC the RAM macros infer to flip-flops (a cache DUT lands at a million cells +# and zero SRAM area) and without SYNTHESIS the simulation-only constructs come +# along. A caller that sets CONFIGS on the MAKE COMMAND LINE would otherwise kill +# every `CONFIGS +=` in this file silently -- command-line variables win over +# `+=`, and nothing warns. Extra defines belong in EXTRA_CONFIGS below. +override CONFIGS += -DSYNTHESIS -DASIC -DYOSYS # Caller-supplied extra design macros (e.g. enabling extensions on a stock top). # Folded in before XCONFIGS so the conditional include blocks below pick them up. -CONFIGS += $(EXTRA_CONFIGS) +override CONFIGS += $(EXTRA_CONFIGS) XCONFIGS := $(shell python3 $(ROOT_DIR)/ci/gen_config.py --config=$(VORTEX_HOME)/VX_config.toml --cflags='$(CONFIGS) -DVX_CFG_XLEN=$(XLEN)') @@ -253,6 +264,8 @@ timing: $(SYN_SRC_DEPS) $(SYN_LIB_DEPS) | $(BUILD_DIR) TOOL_DIR=$(SCRIPT_DIR) \ RUN_STA=1 \ $(SRC_DIR)/run_synth.sh + @$(SRC_DIR)/synth_summary.py --reports $(BUILD_DIR)/reports \ + --top $(TOP_LEVEL_ENTITY) --clock-mhz $(CLOCK_FREQ) gate-sim: techmap asap7-models @if [ "$(PDK)" != "asap7" ]; then echo "ERROR: gate-sim requires PDK=asap7 functional models" >&2; exit 1; fi diff --git a/hw/syn/yosys/dut/Makefile b/hw/syn/yosys/dut/Makefile new file mode 100644 index 0000000000..ec0fa4ce02 --- /dev/null +++ b/hw/syn/yosys/dut/Makefile @@ -0,0 +1,41 @@ +# Per-DUT entry point for the Yosys/OpenSTA flow. +# +# One Makefile, no subdirectories: a DUT is a top module plus an include path +# plus defines (see catalog.mk), forwarded to ../Makefile. The same command a +# human runs is the one asic_gate runs, so the two cannot diverge. +# +# make -C hw/syn/yosys/dut gfx synthesize + STA (TARGET=timing) +# make -C hw/syn/yosys/dut tcu TARGET=synthesis synthesis only +# make -C hw/syn/yosys/dut om CLOCK_FREQ=500 override the target clock +# make -C hw/syn/yosys/dut om CONFIGS=-DFOO extra defines, on top of the DUT's +# make -C hw/syn/yosys/dut list what is available +# +# The DUT's defines go down as EXTRA_CONFIGS, never as CONFIGS: ../Makefile +# accumulates its own mandatory defines (SYNTHESIS/ASIC/YOSYS) with `+=`, and a +# CONFIGS on a make command line overrides that whole chain silently. +# +# PREFIX defaults to the DUT name so each DUT gets its own build tree. That is +# not cosmetic: hw/syn/yosys/Makefile caches $(BUILD_DIR)/src and does not +# regenerate it when EXTRA_INCLUDE changes, so sharing one tree across DUTs +# silently reuses the previous DUT's source set. Override it to keep an +# automated sweep's trees away from a hand-run build of the same DUT (ci/ +# asic_gate.py passes PREFIX=asic_gate_). + +ROOT_DIR := $(realpath ../../../..) +include $(ROOT_DIR)/config.mk + +include catalog.mk + +TARGET ?= timing +PREFIX ?= + +.PHONY: list $(DUTS) + +$(DUTS): + $(MAKE) -C .. $(TARGET) PREFIX=$(or $(PREFIX),$@) \ + TOP_LEVEL_ENTITY=$($@_TOP) \ + EXTRA_INCLUDE="$($@_INC)" \ + EXTRA_CONFIGS="$($@_CFG) $(CONFIGS)" + +list: + @echo $(DUTS) | tr ' ' '\n' diff --git a/hw/syn/yosys/dut/catalog.mk b/hw/syn/yosys/dut/catalog.mk new file mode 100644 index 0000000000..197f7804e8 --- /dev/null +++ b/hw/syn/yosys/dut/catalog.mk @@ -0,0 +1,73 @@ +# DUT catalog for the Yosys/OpenSTA synthesis flow. +# +# The Yosys flow needs no per-DUT Makefile: hw/syn/yosys/Makefile already +# includes hw/syn/extensions.mk, which derives each extension's RTL_PKGS and +# include paths from the resolved XCONFIGS. A DUT is therefore just three +# values -- top module, extra include, defines -- forwarded to that flow. +# +# Tops are the same hw/unittest/*/VX_*_top.sv wrappers the Xilinx gate +# synthesizes, so both gates measure the same modules and a divergence between +# them is meaningful rather than an artefact of two DUT lists. + +DUTS := cache core tcu gfx tex raster om rtu dxa vm tensor vortex + +UNITTEST_DIR ?= $(VORTEX_HOME)/hw/unittest + +cache_TOP := VX_cache_top +cache_INC := -I$(UNITTEST_DIR)/cache +cache_CFG := -DVX_CFG_EXT_A_ENABLE + +core_TOP := VX_core_top +core_INC := -I$(UNITTEST_DIR)/core +core_CFG := -DVX_CFG_NUM_THREADS=16 -DVX_CFG_NUM_WARPS=16 -DVX_CFG_EXT_C_ENABLE -DVX_CFG_EXT_A_ENABLE + +tcu_TOP := VX_tcu_unit_top +tcu_INC := -I$(UNITTEST_DIR)/tcu_unit +tcu_CFG := -DVX_CFG_NUM_THREADS=16 -DVX_CFG_NUM_WARPS=16 -DVX_CFG_EXT_TCU_ENABLE \ + -DVX_CFG_TCU_TYPE_TFR -DVX_CFG_TCU_FP16_ENABLE -DVX_CFG_TCU_FP8_ENABLE \ + -DVX_CFG_TCU_INT8_ENABLE -DVX_CFG_TCU_INT4_ENABLE -DVX_CFG_TCU_TF32_ENABLE \ + -DVX_CFG_TCU_SPARSE_ENABLE -DVX_CFG_TCU_WGMMA_ENABLE + +gfx_TOP := VX_gfx_top +gfx_INC := -I$(UNITTEST_DIR)/gfx +gfx_CFG := -DVX_CFG_EXT_TEX_ENABLE -DVX_CFG_EXT_RTU_ENABLE -DVX_CFG_EXT_RASTER_ENABLE \ + -DVX_CFG_EXT_OM_ENABLE -DVX_CFG_EXT_DXA_ENABLE -DVX_CFG_RASTER_EARLYZ_ENABLE \ + -DVX_CFG_NUM_THREADS=16 -DVX_CFG_NUM_WARPS=16 -DVX_CFG_NUM_CORES=2 \ + -DVX_CFG_SOCKET_SIZE=2 -DVX_CFG_PLATFORM_MEMORY_NUM_BANKS=1 \ + -DVX_CFG_L2_ENABLE -DVX_CFG_L2_SIZE=262144 + +tex_TOP := VX_tex_core_top +tex_INC := -I$(UNITTEST_DIR)/tex_core +tex_CFG := -DVX_CFG_NUM_THREADS=16 -DVX_CFG_NUM_WARPS=16 -DVX_CFG_EXT_TEX_ENABLE + +raster_TOP := VX_raster_core_top +raster_INC := -I$(UNITTEST_DIR)/raster_core +raster_CFG := -DVX_CFG_NUM_THREADS=16 -DVX_CFG_NUM_WARPS=16 -DVX_CFG_EXT_RASTER_ENABLE + +om_TOP := VX_om_core_top +om_INC := -I$(UNITTEST_DIR)/om_core +om_CFG := -DVX_CFG_NUM_THREADS=16 -DVX_CFG_NUM_WARPS=16 -DVX_CFG_EXT_OM_ENABLE + +rtu_TOP := VX_rtu_core_top +rtu_INC := -I$(UNITTEST_DIR)/rtu_core +rtu_CFG := -DVX_CFG_NUM_THREADS=16 -DVX_CFG_NUM_WARPS=16 -DVX_CFG_EXT_RTU_ENABLE + +dxa_TOP := VX_dxa_core_top +dxa_INC := -I$(UNITTEST_DIR)/dxa_core +dxa_CFG := -DVX_CFG_NUM_THREADS=16 -DVX_CFG_NUM_WARPS=16 -DVX_CFG_EXT_DXA_ENABLE + +vm_TOP := VX_vm_top +vm_INC := -I$(UNITTEST_DIR)/vm +vm_CFG := -DVX_CFG_NUM_THREADS=16 -DVX_CFG_NUM_WARPS=16 -DVX_CFG_NUM_CORES=2 -DVX_CFG_VM_ENABLE + +tensor_TOP := VX_tensor_top +tensor_INC := -I$(UNITTEST_DIR)/tensor +tensor_CFG := -DVX_CFG_NUM_THREADS=16 -DVX_CFG_NUM_WARPS=16 -DVX_CFG_EXT_TCU_ENABLE \ + -DVX_CFG_EXT_DXA_ENABLE -DVX_CFG_TCU_WGMMA_ENABLE -DVX_CFG_TCU_MX_ENABLE \ + -DVX_CFG_EXT_A_ENABLE -DVX_CFG_NUM_CORES=2 -DVX_CFG_SOCKET_SIZE=2 \ + -DVX_CFG_PLATFORM_MEMORY_NUM_BANKS=1 -DVX_CFG_L2_ENABLE -DVX_CFG_L2_SIZE=262144 + +# The whole GPU. Hours-long; nightly only, never a PR gate. +vortex_TOP := Vortex +vortex_INC := +vortex_CFG := diff --git a/hw/syn/yosys/run_sta.tcl b/hw/syn/yosys/run_sta.tcl index 46ff314b7c..ffe48d3d3d 100644 --- a/hw/syn/yosys/run_sta.tcl +++ b/hw/syn/yosys/run_sta.tcl @@ -71,10 +71,22 @@ report_units set clks [get_clocks *] if {[llength $clks] > 0} { report_clock_properties $clks } else { puts "No clocks defined." } -report_wns -report_tns -report_wns > [file join $RPT_DIR "wns.rpt"] -report_tns > [file join $RPT_DIR "tns.rpt"] +# -digits 4, not the default 2: ABC maps to the target period and stops, so the +# design closes with picoseconds of margin (0.032 ns on a 1.25 ns period is +# typical). At 2 digits that rounds to 0.00 and "met with margin" becomes +# indistinguishable from "missed by 4 ps" -- which is the whole signal, since +# Fmax is derived from this slack. +report_wns -digits 4 +report_tns -digits 4 +report_wns -digits 4 > [file join $RPT_DIR "wns.rpt"] +report_tns -digits 4 > [file join $RPT_DIR "tns.rpt"] + +# report_wns is worst NEGATIVE slack: it clamps at 0, so a design that closes +# reports 0 no matter how much margin it has, and an Fmax derived from it is +# just the target clock read back. report_worst_slack is signed, so it is what +# says how fast the netlist can actually go. +report_worst_slack -digits 4 +report_worst_slack -digits 4 > [file join $RPT_DIR "worst_slack.rpt"] # Keep report_checks options conservative for 2.7.0 compatibility report_checks -path_delay max -digits 3 -format full_clock_expanded diff --git a/hw/syn/yosys/synth_summary.py b/hw/syn/yosys/synth_summary.py new file mode 100755 index 0000000000..4496c3c1a8 --- /dev/null +++ b/hw/syn/yosys/synth_summary.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Collapse the Yosys/OpenSTA report set into one synth_summary.csv. + +The Xilinx flow's project.tcl already emits a single synth_summary.csv, and +ci/fpga_gate.py reads only that file. The Yosys flow instead scatters its +numbers across reports/{stat_lib,sram_area,wns,tns,power}.rpt, so a gate over +this flow would need a parser per report. Emitting the same one-row CSV here +keeps report-format knowledge next to the tool that produces it, and lets the +gate stay tool-neutral. + +Columns mirror the Xilinx summary where the concept survives the FPGA->ASIC +change (fmax_mhz, wns_ns) and replace it where it does not: LUT/DSP/BRAM give +way to cell_area_um2 / sram_area_um2 / cell_count / seq_area_um2. + + usage: synth_summary.py --reports --top --clock-mhz + [--output ] +""" + +import argparse +import csv +import os +import re +import sys + +COLUMNS = ("top", "clock_mhz", "fmax_mhz", "wns_ns", "tns_ns", "cell_area_um2", + "seq_area_um2", "sram_area_um2", "cell_count", "power_mw") + + +def read(path): + try: + with open(path, errors="replace") as fh: + return fh.read() + except OSError: + return "" + + +def slack(path): + """OpenSTA's slack reports emit a single ' max ' line.""" + m = re.search(r"^\s*(?:worst slack|wns|tns)\s+max\s+(-?[\d.eE+]+)", + read(path), re.M) + return float(m.group(1)) if m else None + + +def area(path): + """Total and sequential cell area, plus cell count, from `stat -liberty`.""" + text = read(path) + out = {} + m = re.search(r"Chip area for module\s+'\\?[^']*':\s*([\d.]+)", text) + if m: + out["cell_area_um2"] = round(float(m.group(1)), 3) + m = re.search(r"used for sequential elements:\s*([\d.]+)", text) + if m: + out["seq_area_um2"] = round(float(m.group(1)), 3) + # " 46213 5.24E+03 cells" -- count is the first column of the `cells` row. + m = re.search(r"^\s*(\d+)\s+\S+\s+cells\s*$", text, re.M) + if m: + out["cell_count"] = int(m.group(1)) + return out + + +def sram(path): + m = re.search(r"TOTAL SRAM ESTIMATED AREA:\s*([\d.]+)", read(path)) + return round(float(m.group(1)), 3) if m else None + + +def power(path): + """Total power in mW. OpenSTA's report_power prints Watts.""" + m = re.search(r"^Total\s+\S+\s+\S+\s+\S+\s+([\d.eE+-]+)", read(path), re.M) + return round(float(m.group(1)) * 1000.0, 6) if m else None + + +def main(argv=None): + ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + ap.add_argument("--reports", required=True, help="reports/ directory") + ap.add_argument("--top", required=True, help="top module name") + ap.add_argument("--clock-mhz", type=float, required=True, + help="target clock the design was constrained to") + ap.add_argument("--output", help="default: /../synth_summary.csv") + args = ap.parse_args(argv) + + rpt = args.reports + row = {"top": args.top, "clock_mhz": args.clock_mhz} + row.update(area(os.path.join(rpt, "stat_lib.rpt"))) + row["sram_area_um2"] = sram(os.path.join(rpt, "sram_area.rpt")) + row["power_mw"] = power(os.path.join(rpt, "power.rpt")) + # Signed worst slack, from report_worst_slack. NOT report_wns: that one is + # worst *negative* slack and clamps at 0, so every design that closes reports + # 0 and the Fmax below would just be the target clock read back. ABC maps to + # the target period and stops, so closing with a few picoseconds of margin is + # the normal outcome and the sign is the whole signal. wns.rpt is the + # fallback for an OpenSTA without report_worst_slack. + row["wns_ns"] = slack(os.path.join(rpt, "worst_slack.rpt")) + if row["wns_ns"] is None: + row["wns_ns"] = slack(os.path.join(rpt, "wns.rpt")) + row["tns_ns"] = slack(os.path.join(rpt, "tns.rpt")) + + # Achieved frequency from the constrained period and its worst slack. A + # positive WNS means the design closed with margin, so Fmax is above target; + # negative means it did not meet the clock it was built for. + if row["wns_ns"] is not None and args.clock_mhz: + period = 1000.0 / args.clock_mhz + achieved = period - row["wns_ns"] + row["fmax_mhz"] = round(1000.0 / achieved, 3) if achieved > 0 else None + else: + row["fmax_mhz"] = None + + out = args.output or os.path.join(os.path.dirname(rpt.rstrip("/")) or ".", + "synth_summary.csv") + with open(out, "w", newline="") as fh: + w = csv.DictWriter(fh, fieldnames=COLUMNS) + w.writeheader() + w.writerow({k: ("" if row.get(k) is None else row[k]) for k in COLUMNS}) + + missing = [k for k in ("cell_area_um2", "wns_ns") if row.get(k) is None] + if missing: + print("WARNING: %s: no %s in %s" % (out, ", ".join(missing), rpt), + file=sys.stderr) + print("synth_summary.csv area=%s um^2 Fmax=%s MHz WNS=%s ns cells=%s" + % (row.get("cell_area_um2"), row.get("fmax_mhz"), row.get("wns_ns"), + row.get("cell_count"))) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 11de2c916f2c07c6e1a8b3499d5abfd9d9ce7c15 Mon Sep 17 00:00:00 2001 From: Blaise Tine Date: Tue, 1 Sep 2026 17:48:02 -0700 Subject: [PATCH 08/10] ci: factor the synthesis gate into a tool-neutral core plus adapters fpga_gate.py was 856 lines of which the great majority -- catalog loading, config_hash, threshold resolution, known_issue, the resumable session, the scheduler, the gate and the report -- says nothing about Vivado. Only the metric names, the build invocation, the log markers and "what makes two runs comparable" do. Those move behind a Tool adapter in ci/synth_gate.py; fpga_gate.py becomes an entry point that pins --tool=xilinx. The Yosys adapter lands in the next commit. No behaviour change, verified against the previous script rather than argued: identical --list output, identical config_hash for all twelve builds (11 of 12 still matching their recorded baselines -- tensor was already stale from the impl_strategy addition, under both versions), and identical verdicts and report tables across unchanged/worse/better metrics, record mode, and the build-failure path. Co-Authored-By: Claude Opus 5 --- ci/fpga_gate.py | 840 +------------------------------- ci/synth_gate.py | 1197 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1206 insertions(+), 831 deletions(-) create mode 100755 ci/synth_gate.py diff --git a/ci/fpga_gate.py b/ci/fpga_gate.py index 5beb34a5cf..3bb07792c5 100755 --- a/ci/fpga_gate.py +++ b/ci/fpga_gate.py @@ -1,23 +1,18 @@ #!/usr/bin/env python3 """fpga_gate — FPGA synthesis-regression gate (Xilinx/Vivado DUT builds). -Same shape as the perf_gate (ci/perf_baseline.py): a checked-in golden baseline -per build, a hard threshold around it, and baselines that only a human updates. -The assertion differs: instead of rtlsim cycles it compares THIS commit's -post-implementation Fmax and LUT count against the recorded numbers, so an RTL -change that quietly costs timing closure or area cannot land green. +Compares THIS commit's post-implementation Fmax and LUT count against a +checked-in golden baseline, so an RTL change that quietly costs timing closure +or area cannot land green. The gating machinery is shared with the ASIC gate +(ci/asic_gate.py) and lives in ci/synth_gate.py; this entry point only pins the +tool. Spec : ci/testcases/fpga_gate.yaml (what to build) Baselines : ci/baselines/synthesis/xilinx/.json (last accepted metrics) - Builds : hw/syn/xilinx/dut// (Vivado, out-of-tree) + Builds : hw/syn/xilinx/dut/fpga_gate_ (Vivado, out-of-tree) Metrics : /synth_summary.csv, timing.rpt, high_fanout_nets.rpt (all three already written by hw/syn/xilinx/dut/project.tcl) -Same division as every other check in the catalog: the yaml is hand-authored, -commented and reviewed; the baseline is machine-written and never hand-edited. -A `config_hash` (dut/clock/configs/device/opt-level/xlen) ties the two together, -so metrics recorded under one config can never be compared against another. - Usage (from a source checkout, with the Xilinx env sourced): source ~/dev/xilinx_setup.sh ci/fpga_gate.py --list @@ -29,828 +24,11 @@ See docs/designs/continuous_integration.md 3.5. """ -import argparse -import concurrent.futures -import csv -import hashlib -import json import os -import re -import shutil -import subprocess import sys -import time - -import yaml - -# Fmax/LUT must stay within +/-5% of baseline. The regression side is the gate; -# the improvement side is the ratchet -- a win beyond it must be recorded so the -# gain is locked in and a later silent slide back to the old number still fails. -# -# This is the GLOBAL default. It is overridden per-metric on the command line -# (--metric-threshold lut=0.10) and per-build in the catalog ("thresholds": -# {"fmax_mhz": 0.03}) -- most specific wins. Same shape as model_parity's -# `tolerance` (per-case entry > category defaults > DEFAULT_PARITY_TOLERANCE). -TOLERANCE = 0.05 - -# Metrics gated by default; the rest are recorded and reported, not asserted. -GATED_METRICS = ("fmax_mhz", "lut") - -# Higher-is-better metrics. Everything else is lower-is-better. -HIGHER_IS_BETTER = ("fmax_mhz",) - -# Verdicts that do not fail the run. KNOWN-ISSUE is a tracked expected failure -# (see gate()); XPASS is one that stopped reproducing -- reported loudly, but a -# gate that hard-failed on an unexpected PASS would be the wrong incentive. -PASSING = ("PASS", "RECORDED", "KNOWN-ISSUE", "XPASS") - -# Reported (and recorded) in this order. build_time_s is bookkeeping -- it is how -# long the build took, and it is what the scheduler uses to order the queue. -METRICS = ("fmax_mhz", "wns_ns", "lut", "lutram", "ff", "bram", "uram", "dsp", - "build_time_s") - -# Top critical paths, read from the timing.rpt project.tcl already writes. -# Recorded whether or not timing closed, and never gated: they are the diagnostic -# that says WHERE the design is tight, so a Fmax regression comes with its -# location instead of just its magnitude. -TIMING_RPT = "timing.rpt" -NUM_PATHS = 10 - -# Top high-fanout nets (high_fanout_nets.rpt). Recorded, never gated -- the -# "why" behind a route-bound critical path. -FANOUT_RPT = "high_fanout_nets.rpt" -NUM_FANOUT = 10 - -# Scheduling weights (seconds) for a build with no recorded build_time_s yet. -# Only ever used to ORDER the queue, so a rough magnitude is enough. -DEFAULT_WEIGHTS = {"top": 14400, "core": 7200, "tcu": 3600, "rtu": 3600, - "vortex": 3600, "cache": 1800, "raster": 1800, "dxa": 1800, - "tex": 1200, "om": 1200} -FALLBACK_WEIGHT = 1800 - -# Vivado jobs per build, clamped so N parallel builds do not oversubscribe. -MIN_VIVADO_JOBS = 2 -MAX_VIVADO_JOBS = 8 - -# Vivado's own log lines around the end of RTL elaboration -- the point past -# which a build can no longer die of a config/RTL typo (see watch()). Vivado -# prints the ELAB_DONE timing line on the way out of elaboration EITHER WAY, so -# a clean pass is ELAB_DONE with no ELAB_FAIL and no error behind it. -ELAB_DONE = "Finished RTL Elaboration" -ELAB_FAIL = "RTL Elaboration failed" - -# Vivado log lines that mark a phase transition, in order. Progress is reported -# against these, so a multi-hour build says where it actually is. -PHASES = ( - ("create_project", "setup"), - ("Starting synth_design", "elaboration"), - (ELAB_DONE, "synthesis"), - ("Synth Design complete", "opt"), - ("Starting Logic Optimization Task", "opt"), - ("Starting Placer Task", "placement"), - ("Starting Routing Task", "routing"), - ("report_timing", "reporting"), -) - -POLL_INTERVAL = 5 -HEARTBEAT = 300 # seconds between "still alive, here is where it is" lines - -# Per-build resume stamp, written into the build dir (not a central session -# file): the state belongs next to the build tree it describes, so it survives a -# kill, a reboot, or a concurrent run touching a different build. -STAMP = "fpga_gate.json" - - -class BuildError(Exception): - pass - - -# --------------------------------------------------------------------------- -# catalog -# --------------------------------------------------------------------------- - -def source_root(): - """Source checkout root (has configure + VX_config.toml), not a build tree.""" - d = os.path.dirname(os.path.abspath(__file__)) - while d != "/": - if os.path.exists(os.path.join(d, "configure")) and \ - os.path.exists(os.path.join(d, "VX_config.toml")): - return d - d = os.path.dirname(d) - sys.exit("ERROR: not inside a Vortex source checkout") - - -def spec_path(root, gate): - return os.path.join(root, "ci", "testcases", gate + ".yaml") - - -def baseline_dir(root, tool): - return os.path.join(root, "ci", "baselines", "synthesis", tool) - - -def baseline_path(root, tool, group): - return os.path.join(baseline_dir(root, tool), group + ".json") - - -def load_catalog(root, gate, tool): - """Builds keyed by id: the SPEC from ci/testcases/.yaml, joined to its - recorded metrics from ci/baselines/synthesis//.json. - - Same division as every other check in the catalog: the yaml says what to run - (and is hand-authored, commented, reviewed), the baseline says what the last - accepted answer was (and is machine-written). Neither file can be mistaken - for the other's job. - """ - with open(spec_path(root, gate)) as fh: - doc = yaml.safe_load(fh) - defaults = doc.get("defaults") or {} - # `builds:`, not `tests:` -- a Vivado build is not a pytest case (no driver, - # no sim build tree, no xlen expansion, no app). The catalog's `tests:` block - # holds the ONE case that invokes this script; the build list is ours. - spec = doc.get("builds") - if not spec: - sys.exit("ERROR: no `builds:` block in %s" % spec_path(root, gate)) - - baselines = {} - for group in {e.get("group", "default") for e in spec}: - path = baseline_path(root, tool, group) - if os.path.exists(path): - with open(path) as fh: - baselines[group] = json.load(fh) - - builds = {} - for entry in spec: - bid = entry["id"] - if bid in builds: - sys.exit("ERROR: duplicate build id '%s' in %s" - % (bid, spec_path(root, gate))) - missing = {"dut", "clock_mhz", "configs"} - set(entry) - if missing: - sys.exit("ERROR: %s: missing %s" % (bid, ", ".join(sorted(missing)))) - group = entry.get("group", "default") - build = dict(entry) - build["group"] = group - build["known_issue"] = entry.get("known_issue", - defaults.get("known_issue", "")) - build["thresholds"] = entry.get("thresholds", - defaults.get("thresholds", {})) - recorded = baselines.get(group, {}).get(bid, {}) - result = recorded.get("result") or {} - # An all-null skeleton means "never recorded" -- the file ships with the - # shape visible, not with numbers. - build["baseline"] = result if result.get("fmax_mhz") is not None else None - build["baseline_env"] = result.get("env") or {} - build["config_hash"] = recorded.get("config_hash") - builds[bid] = build - return builds - - -def select(builds, selectors): - """Resolve --build selectors: an exact build id wins over a group of the - same name; otherwise a group selects all its builds.""" - if not selectors: - return [builds[b] for b in sorted(builds)] - picked = {} - for sel in selectors: - hits = [b for b in builds.values() if b["id"] == sel] - if not hits: - hits = [b for b in builds.values() if b["group"] == sel] - if not hits: - sys.exit("ERROR: no build or group named '%s' (see --list)" % sel) - for b in hits: - picked[b["id"]] = b - return [picked[b] for b in sorted(picked)] - - -def config_hash(build, env): - """Fingerprint the synthesis inputs; a change invalidates the stored metrics.""" - key = "|".join([build["dut"], str(build["clock_mhz"]), build["configs"], - env["device"], str(env["opt_level"]), str(env["xlen"])]) - # Appended only when set, so builds without a strategy override keep the - # hash their baselines were recorded under. - if build.get("impl_strategy"): - key += "|" + build["impl_strategy"] - return hashlib.sha256(key.encode()).hexdigest()[:16] - - -def save_baselines(root, tool, results, env): - """Write measured metrics to the golden baselines (never done by CI). - - Each entry records the GPU configuration it represents -- dut, target clock, - and configs -- so a baseline is meaningful on its own: a set of metrics with - no identifiable config is worthless. The yaml `builds:` block stays the - source of truth for what to build; config_hash fingerprints that spec and - invalidates the metrics if it drifts, so the recorded config cannot be - hand-edited to disagree with the numbers next to it without failing the gate. - """ - by_group = {} - for r in results: - by_group.setdefault(r["build"]["group"], []).append(r) - os.makedirs(baseline_dir(root, tool), exist_ok=True) - for group, rs in by_group.items(): - path = baseline_path(root, tool, group) - entries = {} - if os.path.exists(path): - with open(path) as fh: - entries = json.load(fh) - for r in rs: - b = r["build"] - entries[b["id"]] = { - "dut": b["dut"], - "clock_mhz": b["clock_mhz"], - "configs": b["configs"], - "config_hash": config_hash(b, env), - "result": dict(r["metrics"], env=dict(env)), - } - if b.get("impl_strategy"): - entries[b["id"]]["impl_strategy"] = b["impl_strategy"] - with open(path, "w") as fh: - json.dump(entries, fh, indent=2, sort_keys=True) - fh.write("\n") - print("updated %s" % path) - - -# --------------------------------------------------------------------------- -# build -# --------------------------------------------------------------------------- - -def prepare_build_tree(root, build_dir, xlen): - """Configure the gate's own out-of-tree build if it is not there yet.""" - if os.path.exists(os.path.join(build_dir, "config.mk")): - return - tooldir = os.environ.get("TOOLDIR") or os.path.expanduser("~/tools") - os.makedirs(build_dir, exist_ok=True) - print("configuring build tree %s (xlen=%d, tooldir=%s)" - % (build_dir, xlen, tooldir)) - subprocess.run([os.path.join(root, "configure"), - "--tooldir=%s" % tooldir, "--xlen=%d" % xlen], - cwd=build_dir, check=True) - - -def read_summary(path): - """Parse project.tcl's synth_summary.csv into the metric dict.""" - with open(path) as fh: - row = next(csv.DictReader(fh)) - out = {} - for m in METRICS: - if m in row: - out[m] = round(float(row[m]), 3) if m in ("fmax_mhz", "wns_ns") \ - else int(float(row[m])) - return out - - -def read_paths(path): - """Top critical paths, parsed from Vivado's timing.rpt. - - project.tcl already writes one (`report_timing -unique_pins -nworst 100`), so - this needs no extra Vivado work and can be re-read from any finished build. - -unique_pins is what makes the list 10 REAL paths rather than 10 views of the - same one through different pins. - - The logic/route split is kept because it is the diagnosis, not decoration: a - path that is 66% route is placement/congestion, and no amount of retiming the - logic will fix it. - """ - if not os.path.exists(path): - return [] - paths, cur = [], None - with open(path, errors="replace") as fh: - for line in fh: - line = line.strip() - m = re.match(r"Slack \((MET|VIOLATED)\)\s*:\s*(-?[\d.]+)ns", line) - if m: - if cur: - paths.append(cur) - if len(paths) >= NUM_PATHS: - break - cur = {"slack_ns": float(m.group(2))} - elif cur is None: - continue - elif line.startswith("Source:"): - cur["startpoint"] = line.split(None, 1)[1] - elif line.startswith("Destination:"): - cur["endpoint"] = line.split(None, 1)[1] - elif line.startswith("Path Group:"): - cur["group"] = line.split(None, 2)[2] - elif line.startswith("Logic Levels:"): - cur["levels"] = int(line.split()[2]) - elif line.startswith("Data Path Delay:"): - d = re.search(r"logic ([\d.]+)ns .*?route ([\d.]+)ns \(([\d.]+)%\)", - line) - if d: - cur["logic_ns"] = float(d.group(1)) - cur["route_ns"] = float(d.group(2)) - cur["route_pct"] = float(d.group(3)) - if cur and len(paths) < NUM_PATHS: - paths.append(cur) - return paths - - -def read_fanout(path): - """Top high-fanout nets, from the high_fanout_nets.rpt project.tcl writes. - - Recorded, never gated. A net fanning out to thousands of loads is what forces - the placer to spread its cone across the die, which is the routing delay the - critical paths then eat -- so this is the other half of the same diagnosis: - the paths say WHERE the design is tight, these say WHY. - """ - if not os.path.exists(path): - return [] - nets = [] - for line in open(path, errors="replace"): - cols = [c.strip() for c in line.split("|")[1:-1]] - if len(cols) != 3 or not cols[1].isdigit(): - continue # header, rule, or a non-table line - nets.append({"net": cols[0], "fanout": int(cols[1]), "driver": cols[2]}) - if len(nets) >= NUM_FANOUT: - break - return nets - - -def threshold(build, metric, args): - """Gate tolerance for one metric: entry > per-metric global > global.""" - return float((build.get("thresholds") or {}).get( - metric, args.metric_threshold.get(metric, args.threshold))) - - -def say(build, tag, msg, t0=None): - at = "" if t0 is None else " (%dm)" % ((time.monotonic() - t0) // 60) - print(" [%-5s] %-10s %s%s" % (tag, build["id"], msg, at)) - - -def watch(proc, log, build, timeout, verbose, seen=0): - """Follow a running build's log: report phases, announce elaboration, collect errors. - - A config mistake -- a bad define, a missing source, a parameter or hierarchy - error -- dies during RTL elaboration, seconds into synthesis. Passing - ELAB_DONE is therefore the point where a build stops being a typo risk and - starts being an hours-long run worth waiting for, so it is announced as soon - as it lands rather than at the end. Note Vivado prints ELAB_DONE on the way - out of elaboration whether it passed or failed, so a clean pass also requires - no ELAB_FAIL and no error behind it. - - Returns (rc, reached_elab, errors). - """ - reached = failed = False - errors, phase, t0, beat = [], None, time.monotonic(), time.monotonic() - while True: - rc = proc.poll() - if os.path.exists(log): - with open(log, errors="replace") as fh: - fh.seek(seen) - for line in fh: - if verbose: - print(" | %-10s %s" % (build["id"], line.rstrip())) - if ELAB_FAIL in line: - failed = True - elif ELAB_DONE in line and not (failed or errors): - reached = True - say(build, "elab", "past RTL elaboration -- into " - "synthesis", t0) - for marker, label in PHASES: - if marker in line and label != phase: - phase = label - if not verbose: # verbose already streams the line - say(build, "phase", label, t0) - if line.startswith("ERROR:") and len(errors) < 5: - errors.append(line.strip()) - seen = fh.tell() - if rc is not None: - return rc, reached, errors - now = time.monotonic() - if now - t0 > timeout: - proc.kill() - raise BuildError("timed out after %ds (elaboration %s)" - % (timeout, "passed" if reached else "NOT reached")) - if not verbose and now - beat >= HEARTBEAT: - beat = now - say(build, "...", "%s, still running" % (phase or "starting"), t0) - time.sleep(POLL_INTERVAL) - - -def load_stamp(work, chash): - """This build dir's resume state, if it belongs to the CURRENT config.""" - try: - with open(os.path.join(work, STAMP)) as fh: - stamp = json.load(fh) - except (OSError, ValueError): - return None - # A stamp from a different config means the checkpoints in this tree are for - # a design we are no longer building -- resuming from them would silently - # synthesize the OLD config. Treat as absent: rebuild from scratch. - return stamp if stamp.get("config_hash") == chash else None - - -def save_stamp(work, chash, status, metrics=None): - with open(os.path.join(work, STAMP), "w") as fh: - json.dump({"config_hash": chash, "status": status, "metrics": metrics}, - fh, indent=2, sort_keys=True) - fh.write("\n") - - -def run_build(build, build_dir, env, args): - """Synthesize one DUT; return its metrics. Raises BuildError on failure.""" - dut_root = os.path.join(build_dir, "hw", "syn", "xilinx", "dut") - src = os.path.join(dut_root, build["dut"]) - if not os.path.isdir(src): - raise BuildError("no DUT target '%s' under %s" % (build["dut"], dut_root)) - - # Unique PREFIX per build id: its own build tree and its own log, so parallel - # builds (and other synthesis runs on this machine) cannot collide. - prefix = "fpga_gate_" + build["id"] - work = os.path.join(src, prefix) - chash = config_hash(build, env) - - # Resume: a finished build of THIS config is reused as-is; an unfinished one - # keeps its build tree so Vivado picks up from its post-synth/post-impl - # checkpoint (project.tcl resumes on either) instead of re-running hours of - # work. Anything else -- no stamp, or a stamp for a different config -- is a - # clean rebuild. - resume = False - if args.resume: - stamp = load_stamp(work, chash) - if stamp and stamp["status"] == "done" and stamp["metrics"]: - # A build finished before critical-path reporting existed has no - # paths recorded. Rather than reuse an incomplete result, resume from - # its post-impl checkpoint: project.tcl re-runs only run_report, so - # the paths are backfilled in minutes instead of hours. - if stamp["metrics"].get("critical_paths") and \ - stamp["metrics"].get("high_fanout_nets") is not None: - say(build, "cache", "already built for this config -- reusing") - return stamp["metrics"] - say(build, "resum", "built, but no critical paths recorded -- " - "re-reporting from checkpoint") - resume = True - elif stamp: - resume = True - say(build, "resum", "reusing build tree, resuming from checkpoint") - - os.makedirs(work, exist_ok=True) - shutil.copy2(os.path.join(src, "Makefile"), work) - - benv = dict(os.environ) - benv.update(CONFIGS=build["configs"], - CLK_FREQ_MHZ=str(build["clock_mhz"]), - OPT_LEVEL=str(env["opt_level"]), - MAX_JOBS=str(args.vivado_jobs)) - if build.get("impl_strategy"): - benv["IMPL_STRATEGY"] = build["impl_strategy"] - if resume: - benv["RESUME"] = "1" # make clean is a no-op; keeps project_1 + *.dcp - - log = os.path.join(work, "build.log") - t0 = time.monotonic() - if not resume: - subprocess.run(["make", "-C", work, "clean"], env=benv, - stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, - check=True) - # Stamp BEFORE building: a run killed mid-flight leaves a resumable marker. - save_stamp(work, chash, "running") - - if args.verbose: - say(build, "run", "CONFIGS=%s CLK_FREQ_MHZ=%d OPT_LEVEL=%d MAX_JOBS=%d" - % (build["configs"], build["clock_mhz"], env["opt_level"], - args.vivado_jobs)) - # On resume the log is appended, so start watching past what is already - # there -- otherwise the previous attempt's phases replay as if they were - # this one's (and its old ERROR: lines resurface as this run's failure). - seen = os.path.getsize(log) if resume and os.path.exists(log) else 0 - with open(log, "a" if resume else "w") as fh: - proc = subprocess.Popen(["make", "-C", work], env=benv, stdout=fh, - stderr=subprocess.STDOUT) - rc, reached_elab, errors = watch(proc, log, build, args.timeout, - args.verbose, seen) - elapsed = int(time.monotonic() - t0) - - if rc != 0: - raise BuildError("%s (rc=%d) after %dm%s\n see %s" - % ("FAILED BEFORE SYNTHESIS -- config/RTL error" - if not reached_elab else "build failed", - rc, elapsed // 60, - "".join("\n | " + e for e in errors), log)) - - summary = os.path.join(work, "synth_summary.csv") - if not os.path.exists(summary): - raise BuildError("no synth_summary.csv (Vivado did not reach report), " - "see %s" % log) - metrics = read_summary(summary) - metrics["build_time_s"] = elapsed - metrics["critical_paths"] = read_paths(os.path.join(work, TIMING_RPT)) - metrics["high_fanout_nets"] = read_fanout(os.path.join(work, FANOUT_RPT)) - save_stamp(work, chash, "done", metrics) - return metrics - - -def weight(build): - """Scheduling cost: the recorded build time, else a per-DUT estimate.""" - recorded = (build.get("baseline") or {}).get("build_time_s") - return recorded or DEFAULT_WEIGHTS.get(build["dut"], FALLBACK_WEIGHT) - - -def run_all(builds, build_dir, env, args): - """Longest-processing-time-first list scheduling. - - Submitting longest-first to a fixed pool means a long build is always in - flight from t=0 (it is dispatched first) and the remaining slots churn - through the short ones -- so the makespan is bounded by the longest build - rather than by a short build starting last and running past everything. - """ - order = sorted(builds, key=weight, reverse=True) - print("schedule (%d builds, %d parallel, %d Vivado jobs each):" - % (len(order), args.jobs, args.vivado_jobs)) - for b in order: - print(" %-10s dut=%-8s %4d MHz ~%dm" - % (b["id"], b["dut"], b["clock_mhz"], weight(b) // 60)) - if args.dry_run: - return [] - - results = [] - with concurrent.futures.ThreadPoolExecutor(max_workers=args.jobs) as pool: - futures = {pool.submit(run_build, b, build_dir, env, args): b - for b in order} - for fut in concurrent.futures.as_completed(futures): - b = futures[fut] - try: - metrics = fut.result() - say(b, "done", "Fmax=%.1f MHz LUT=%d (%dm)" - % (metrics["fmax_mhz"], metrics["lut"], - metrics["build_time_s"] // 60)) - results.append({"build": b, "metrics": metrics, "error": None}) - except (BuildError, subprocess.CalledProcessError) as e: - say(b, "FAIL", str(e)) - results.append({"build": b, "metrics": None, "error": str(e)}) - return sorted(results, key=lambda r: r["build"]["id"]) - - -# --------------------------------------------------------------------------- -# gate -# --------------------------------------------------------------------------- - -def gate(result, env, args, gated): - """Compare one result against its baseline; return (verdict, [reasons]). - - A build carrying a `known_issue` reason is tracked-expected-failure: it still - builds, still reports, and its numbers still land in the table -- but its - verdict does not fail the run. Same contract as a `known_issue:` test case - (conftest marks those xfail strict=False), including that an unexpected pass - is surfaced (XPASS) rather than converted into a hard failure. - """ - build = result["build"] - known = build.get("known_issue") - - def verdict(name, reasons): - if not known: - return name, reasons - return "KNOWN-ISSUE", reasons + ["known issue: " + known] - - if result["error"]: - return verdict("BUILD-FAIL", [result["error"]]) - - base = build.get("baseline") or {} - if not any(base.get(m) is not None for m in gated): - return verdict("NO-BASELINE", - ["no recorded baseline (run --update-baseline)"]) - if build.get("config_hash") != config_hash(build, env): - return verdict("STALE", ["config changed since the baseline was recorded " - "(re-record with --update-baseline)"]) - - reasons = [] - for m in gated: - cur, ref = result["metrics"].get(m), base.get(m) - if ref is None: - reasons.append("%s: missing from baseline" % m) - continue - tol = threshold(build, m, args) - delta = (cur - ref) / ref if ref else 0.0 - if abs(delta) <= tol: - continue - better = (delta > 0) == (m in HIGHER_IS_BETTER) - reasons.append("%s: %g -> %g (%+.1f%%, tolerance %.0f%%) -- %s" - % (m, ref, cur, 100 * delta, 100 * tol, - "improvement, update the baseline to lock it in" - if better else "REGRESSION")) - - # Target-frequency gate: Fmax must be within tolerance of the clock the design - # was BUILT for, independent of the baseline. A baseline recorded below target - # (a design that never met its clock) must not let a build pass just by matching - # that number -- the gate answers "does it meet the frequency it targets?", not - # only "did it get worse than last time?". - target = build.get("clock_mhz") - fmax = result["metrics"].get("fmax_mhz") - if target and fmax is not None: - tol = threshold(build, "fmax_mhz", args) - shortfall = (float(target) - fmax) / float(target) - if shortfall > tol: - reasons.append("fmax_mhz: %.1f MHz vs %g MHz target (%.1f%% below, " - "tolerance %.0f%%) -- BELOW TARGET" - % (fmax, target, 100 * shortfall, 100 * tol)) - - if not reasons: - return ("XPASS", ["known issue no longer reproduces -- clear " - "`known_issue` from the catalog"]) if known \ - else ("PASS", []) - return verdict("IMPROVED" if all("improvement" in r for r in reasons) - else "FAIL", reasons) - - -def report(results, env, args, gated, record=False): - """Print the metric table; return the list of failing verdicts. - - In record mode the table is the review artifact for the baseline update, so - the per-metric deltas are still shown (that is the diff a human signs off on) - but they are not verdicts -- recording IS the intent. - """ - print("\n%-10s %-8s %9s %9s %9s %9s %8s %6s %6s %6s %s" - % ("build", "dut", "fmax", "lut", "ff", "lutram", "bram", "uram", - "dsp", "time", "verdict")) - print("-" * 108) - failures = [] - for r in results: - b = r["build"] - if record: - verdict, reasons = ("RECORDED", []) if r["metrics"] \ - else ("BUILD-FAIL", [r["error"]]) - else: - verdict, reasons = gate(r, env, args, gated) - r["verdict"] = verdict - if reasons: # includes KNOWN-ISSUE/XPASS: reported, but not failing - failures.append((b["id"], verdict, reasons)) - if r["metrics"] is None: - print("%-10s %-8s %s" % (b["id"], b["dut"], verdict)) - continue - m, base = r["metrics"], b.get("baseline") or {} - - def cell(key, fmt="%d"): - cur = m.get(key) - if cur is None: - return "-" - ref = base.get(key) - if not ref: - return fmt % cur - return "%s(%+.0f%%)" % (fmt % cur, 100 * (cur - ref) / ref) - - print("%-10s %-8s %9s %9s %9s %9s %8s %6s %6s %5dm %s" - % (b["id"], b["dut"], cell("fmax_mhz", "%.0f"), cell("lut"), - cell("ff"), cell("lutram"), cell("bram"), cell("uram"), - cell("dsp"), m["build_time_s"] // 60, verdict)) - - for bid, verdict, reasons in failures: - print("\n%s: %s" % (bid, verdict)) - for reason in reasons: - print(" - %s" % reason) - return failures - - -# --------------------------------------------------------------------------- - -def main(argv=None): - ap = argparse.ArgumentParser( - description="FPGA synthesis-regression gate (Fmax/area vs golden baseline)") - ap.add_argument("-b", "--build", action="append", default=[], metavar="ID", - help="build id or group to run (repeatable; default: all)") - ap.add_argument("-j", "--jobs", type=int, default=2, metavar="N", - help="max parallel Vivado builds (default: 2)") - ap.add_argument("--vivado-jobs", type=int, default=0, metavar="N", - help="Vivado jobs per build (default: cores/jobs, clamped to " - "%d..%d)" % (MIN_VIVADO_JOBS, MAX_VIVADO_JOBS)) - ap.add_argument("--tool", default="xilinx", choices=("xilinx",), - help="synthesis toolchain; selects the golden file tree " - "ci/baselines/synthesis//") - ap.add_argument("--threshold", type=float, default=TOLERANCE, metavar="F", - help="global gate tolerance, fraction (default: %.2f). " - "Overridden per-metric by --metric-threshold and " - "per-build by the catalog's \"thresholds\"." - % TOLERANCE) - ap.add_argument("--metric-threshold", action="append", default=[], - metavar="M=F", - help="global tolerance for one metric, e.g. lut=0.10 " - "(repeatable); still overridden per-build") - ap.add_argument("--gate", default=",".join(GATED_METRICS), metavar="M,M", - help="metrics to assert on (default: %s); all metrics are " - "always recorded and reported" % ",".join(GATED_METRICS)) - ap.add_argument("--update-baseline", action="store_true", - help="record this run's metrics as the new golden baseline. " - "Human-reviewed; CI must never pass this.") - ap.add_argument("--build-dir", metavar="DIR", - help="build tree to synthesize in (default: " - "/build_fpga_gate, configured on demand)") - ap.add_argument("--xlen", type=int, default=32, choices=(32, 64), - help="ISA width (default: 32)") - ap.add_argument("--opt-level", type=int, default=3, metavar="N", - help="Vivado optimization level, 0=fastest 3=default") - ap.add_argument("--timeout", type=int, default=8 * 3600, metavar="SEC", - help="per-build timeout (default: 8h)") - ap.add_argument("--resume", action="store_true", - help="resume an interrupted session: builds already finished " - "for this config are reused, unfinished ones pick up " - "from their Vivado checkpoint, the rest run. A build " - "whose config changed is rebuilt clean.") - ap.add_argument("-v", "--verbose", action="store_true", - help="stream each build's Vivado log to stdout (prefixed by " - "build id) instead of phase/heartbeat lines") - ap.add_argument("--report", metavar="FILE", - help="write the full run (metrics + verdicts) as JSON") - ap.add_argument("--list", action="store_true", help="list builds and exit") - ap.add_argument("--dry-run", action="store_true", - help="print the schedule without running Vivado") - args = ap.parse_args(argv) - - # Line-buffer stdout: progress is the point of the elaboration watch, and it - # is worthless if it sits in a block buffer until the run ends (which is what - # happens whenever this is piped -- CI, nohup, a log file). - sys.stdout.reconfigure(line_buffering=True) - - overrides = {} - for spec in args.metric_threshold: - metric, _, value = spec.partition("=") - if metric not in METRICS or not value: - sys.exit("ERROR: --metric-threshold expects =, " - "metric one of: %s" % ", ".join(METRICS)) - overrides[metric] = float(value) - args.metric_threshold = overrides - - root = source_root() - builds = load_catalog(root, "fpga_gate", args.tool) - - if args.list: - for b in select(builds, args.build): - base = b.get("baseline") or {} - print("%-10s [%s] dut=%-8s %4d MHz %s%s" - % (b["id"], b["group"], b["dut"], b["clock_mhz"], - "baseline: Fmax=%.0f MHz LUT=%d" - % (base["fmax_mhz"], base["lut"]) - if base.get("fmax_mhz") else "no baseline", - " KNOWN ISSUE: " + b["known_issue"] - if b.get("known_issue") else "")) - print(" %s" % b["configs"]) - return 0 - - if not os.environ.get("XILINX_VIVADO"): - sys.exit("ERROR: XILINX_VIVADO is not set -- source the Xilinx env first " - "(source ~/dev/xilinx_setup.sh)") - - if args.jobs < 1: - sys.exit("ERROR: --jobs must be >= 1") - if not args.vivado_jobs: - args.vivado_jobs = max(MIN_VIVADO_JOBS, - min(MAX_VIVADO_JOBS, os.cpu_count() // args.jobs)) - - build_dir = args.build_dir or os.path.join( - root, "build%d_fpga_gate" % args.xlen) - env = {"device": os.environ.get("DEVICE", "xcu55c-fsvh2892-2L-e"), - "vivado": os.path.basename(os.environ["XILINX_VIVADO"].rstrip("/")), - "opt_level": args.opt_level, "xlen": args.xlen} - - selected = select(builds, args.build) - for b in selected: - ref = b.get("baseline_env", {}).get("vivado") - if ref and ref != env["vivado"]: - print("WARNING: %s baseline was recorded on Vivado %s, running %s -- " - "results across tool versions are not comparable" - % (b["id"], ref, env["vivado"])) - - if not args.dry_run: - prepare_build_tree(root, build_dir, args.xlen) - results = run_all(selected, build_dir, env, args) - if args.dry_run: - return 0 - - gated = tuple(m for m in args.gate.split(",") if m) - outcomes = report(results, env, args, gated, args.update_baseline) - failures = [o for o in outcomes if o[1] not in PASSING] - - ok = [r for r in results if r["metrics"]] - if args.update_baseline and ok: - save_baselines(root, args.tool, ok, env) - - if args.report: - with open(args.report, "w") as fh: - json.dump({"env": env, "tolerance": args.threshold, - "metric_thresholds": args.metric_threshold, - "gated": list(gated), - "builds": [{"id": r["build"]["id"], - "group": r["build"]["group"], - "dut": r["build"]["dut"], - "clock_mhz": r["build"]["clock_mhz"], - "known_issue": r["build"].get("known_issue", ""), - "metrics": r["metrics"], - "error": r["error"], - "verdict": r["verdict"]} - for r in results]}, fh, indent=2) - fh.write("\n") - print("\nwrote %s" % args.report) - - if args.update_baseline: - # Recording IS the intent; a delta is what we just wrote down. Only a - # build that never produced metrics is still a failure -- unless it is a - # tracked known issue. - broken = [r for r in results - if not r["metrics"] and not r["build"].get("known_issue")] - return 2 if broken else 0 - if any(v == "BUILD-FAIL" for _, v, _ in failures): - return 2 - return 1 if failures else 0 +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import synth_gate # noqa: E402 if __name__ == "__main__": - sys.exit(main()) + sys.exit(synth_gate.main(default_tool="xilinx")) diff --git a/ci/synth_gate.py b/ci/synth_gate.py new file mode 100755 index 0000000000..85ecbd9af5 --- /dev/null +++ b/ci/synth_gate.py @@ -0,0 +1,1197 @@ +#!/usr/bin/env python3 +"""synth_gate — synthesis-regression gate over a checked-in golden baseline. + +Same shape as the perf_gate (ci/perf_baseline.py): a golden baseline per build, +a hard threshold around it, and baselines that only a human updates. The +assertion differs: instead of rtlsim cycles it compares THIS commit's +post-synthesis timing and area against the recorded numbers, so an RTL change +that quietly costs timing closure or area cannot land green. + +Two backends, one gate. Everything that is not "how do I drive the tool and what +does it call its numbers" is shared: + + --tool=xilinx Vivado synthesis + place-and-route -> ci/fpga_gate.py + --tool=yosys Yosys + OpenSTA on an ASIC PDK -> ci/asic_gate.py + + Spec : ci/testcases/.yaml (what to build) + Baselines : ci/baselines/synthesis//.json (last accepted metrics) + Metrics : /synth_summary.csv, written by the flow itself + +Same division as every other check in the catalog: the yaml is hand-authored, +commented and reviewed; the baseline is machine-written and never hand-edited. +A `config_hash` (dut/clock/configs/tool env/xlen) ties the two together, so +metrics recorded under one config can never be compared against another. + +See docs/designs/continuous_integration.md 3.5. +""" + +import argparse +import concurrent.futures +import csv +import hashlib +import json +import os +import re +import shutil +import subprocess +import sys +import time + +import yaml + +# Gated metrics must stay within +/-5% of baseline. The regression side is the +# gate; the improvement side is the ratchet -- a win beyond it must be recorded +# so the gain is locked in and a later silent slide back to the old number still +# fails. +# +# This is the GLOBAL default. It is overridden per-metric on the command line +# (--metric-threshold lut=0.10) and per-build in the catalog ("thresholds": +# {"fmax_mhz": 0.03}) -- most specific wins. Same shape as model_parity's +# `tolerance` (per-case entry > category defaults > DEFAULT_PARITY_TOLERANCE). +TOLERANCE = 0.05 + +# Higher-is-better metrics. Everything else is lower-is-better. +HIGHER_IS_BETTER = ("fmax_mhz",) + +# Verdicts that do not fail the run. KNOWN-ISSUE is a tracked expected failure +# (see gate()); XPASS is one that stopped reproducing -- reported loudly, but a +# gate that hard-failed on an unexpected PASS would be the wrong incentive. +PASSING = ("PASS", "RECORDED", "KNOWN-ISSUE", "XPASS") + +POLL_INTERVAL = 5 +HEARTBEAT = 300 # seconds between "still alive, here is where it is" lines + +# Tool jobs per build, clamped so N parallel builds do not oversubscribe. +MIN_TOOL_JOBS = 2 +MAX_TOOL_JOBS = 8 + + +class BuildError(Exception): + pass + + +# --------------------------------------------------------------------------- +# tools +# +# A backend supplies four things and nothing else: what it measures, how it is +# invoked, how its log reads, and what makes two runs comparable. The gating +# core below never names a tool. +# --------------------------------------------------------------------------- + +class Tool: + """One synthesis backend.""" + + name = "" # --tool value, and ci/baselines/synthesis// + gate = "" # ci/testcases/.yaml, and the build-dir/prefix/stamp name + flow = "" # DUT entry point, relative to a configured build tree + + metrics = () # recorded, in report order; build_time_s is bookkeeping + gated = () # asserted by default (--gate overrides) + integral = () # recorded as ints; every other metric is a rounded float + columns = () # (header, metric, format, width) for the report table + + # Scheduling weights (seconds) for a build with no recorded build_time_s yet. + # Only ever used to ORDER the queue, so a rough magnitude is enough. + weights = {} + fallback_weight = 1800 + job_label = "tool" # what --tool-jobs buys, in the schedule banner + + # Log lines that mark a phase transition, in order. Progress is reported + # against these, so a long build says where it actually is. + phases = () + # The point past which a build can no longer die of a config/RTL typo (see + # watch()), and -- where the tool prints one -- the line that says it did. + elab_done = "" + elab_fail = "" + elab_label = "elaboration" + error_re = re.compile(r"^ERROR:") + + def check_env(self): + """Fail fast if the tool is not on this machine.""" + + def env(self, args): + """The tool environment recorded alongside every baseline.""" + raise NotImplementedError + + def hash_key(self, env): + """The env fields that make two runs comparable, for config_hash.""" + raise NotImplementedError + + def env_warning(self, build, env): + """Optional warning when a baseline was recorded under a different tool.""" + return None + + def dut_root(self, build_dir): + return os.path.join(build_dir, *self.flow.split("/")) + + def work_dir(self, dut_root, prefix, build): + raise NotImplementedError + + def prepare_flow(self, dut_root, env): + """Once-per-session setup, before any build starts.""" + + def clean(self, dut_root, work, build, benv): + raise NotImplementedError + + def build_command(self, dut_root, work, build, env, args): + """(argv, extra environment) that runs this build to completion.""" + raise NotImplementedError + + def extras(self, work): + """Diagnostics recorded next to the metrics, never gated.""" + return {} + + def incomplete(self, metrics): + """Whether a cached result predates something this tool now records. + + Reusing it would silently keep the gap forever, so --resume re-runs from + the checkpoint instead -- minutes of reporting, not hours of synthesis. + """ + return False + + def describe(self, metrics): + """One-line result summary for the progress log.""" + return " ".join("%s=%s" % (c[0], metrics.get(c[1])) for c in self.columns) + + def describe_baseline(self, base): + """One-line baseline summary for --list.""" + return " ".join("%s=%s" % (c[0], base.get(c[1])) for c in self.columns) + + +# --- catalog.mk parsing, shared by both DUT trees --------------------------- + +def _catalog(dut_root): + """The DUT catalog text (dut/catalog.mk is the single DUT list per flow).""" + try: + with open(os.path.join(dut_root, "catalog.mk")) as fh: + return fh.read() + except OSError: + return "" + + +def _make_var(text, name): + """One `NAME := value` from a catalog, with line continuations folded in.""" + m = re.search(r"^%s\s*:?=\s*((?:.*\\\n)*.*)$" % re.escape(name), text, re.M) + return m.group(1).replace("\\\n", " ").strip() if m else "" + + +def dut_names(dut_root): + """DUT ids declared by dut/catalog.mk.""" + return set(_make_var(_catalog(dut_root), "DUTS").split()) + + +class Xilinx(Tool): + """Vivado synthesis + place-and-route (hw/syn/xilinx/dut).""" + + name = "xilinx" + gate = "fpga_gate" + flow = "hw/syn/xilinx/dut" + + metrics = ("fmax_mhz", "wns_ns", "lut", "lutram", "ff", "bram", "uram", + "dsp", "build_time_s") + gated = ("fmax_mhz", "lut") + integral = ("lut", "lutram", "ff", "bram", "uram", "dsp", "build_time_s") + columns = (("fmax", "fmax_mhz", "%.0f", 9), ("lut", "lut", "%d", 9), + ("ff", "ff", "%d", 9), ("lutram", "lutram", "%d", 9), + ("bram", "bram", "%d", 8), ("uram", "uram", "%d", 6), + ("dsp", "dsp", "%d", 6)) + + weights = {"top": 14400, "core": 7200, "tcu": 3600, "rtu": 3600, + "vortex": 3600, "cache": 1800, "raster": 1800, "dxa": 1800, + "tex": 1200, "om": 1200} + job_label = "Vivado" + + # Vivado prints the ELAB_DONE timing line on the way out of elaboration + # EITHER WAY, so a clean pass is elab_done with no elab_fail behind it. + elab_done = "Finished RTL Elaboration" + elab_fail = "RTL Elaboration failed" + elab_label = "RTL elaboration" + phases = ( + ("create_project", "setup"), + ("Starting synth_design", "elaboration"), + (elab_done, "synthesis"), + ("Synth Design complete", "opt"), + ("Starting Logic Optimization Task", "opt"), + ("Starting Placer Task", "placement"), + ("Starting Routing Task", "routing"), + ("report_timing", "reporting"), + ) + + # Top critical paths, read from the timing.rpt project.tcl already writes. + # Recorded whether or not timing closed, and never gated: they are the + # diagnostic that says WHERE the design is tight, so a Fmax regression comes + # with its location instead of just its magnitude. + TIMING_RPT = "timing.rpt" + NUM_PATHS = 10 + # Top high-fanout nets (high_fanout_nets.rpt). Recorded, never gated -- the + # "why" behind a route-bound critical path. + FANOUT_RPT = "high_fanout_nets.rpt" + NUM_FANOUT = 10 + + def check_env(self): + if not os.environ.get("XILINX_VIVADO"): + sys.exit("ERROR: XILINX_VIVADO is not set -- source the Xilinx env " + "first (source ~/dev/xilinx_setup.sh)") + + def env(self, args): + return {"device": os.environ.get("DEVICE", "xcu55c-fsvh2892-2L-e"), + "vivado": os.path.basename(os.environ["XILINX_VIVADO"].rstrip("/")), + "opt_level": args.opt_level, "xlen": args.xlen} + + def hash_key(self, env): + return [env["device"], str(env["opt_level"]), str(env["xlen"])] + + def env_warning(self, build, env): + ref = build.get("baseline_env", {}).get("vivado") + if ref and ref != env["vivado"]: + return ("baseline was recorded on Vivado %s, running %s -- results " + "across tool versions are not comparable" % (ref, env["vivado"])) + return None + + def work_dir(self, dut_root, prefix, build): + return os.path.join(dut_root, "%s_%s" % (prefix, build["dut"])) + + def clean(self, dut_root, work, build, benv): + subprocess.run(["make", "-C", work, "clean"], env=benv, + stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, + check=True) + + def build_command(self, dut_root, work, build, env, args): + # build.mk plays the role the former dut//Makefile did: it is + # copied in as the work tree's Makefile and run there with DUT=. + os.makedirs(work, exist_ok=True) + shutil.copy2(os.path.join(dut_root, "build.mk"), + os.path.join(work, "Makefile")) + benv = dict(DUT=build["dut"], + CONFIGS=build["configs"], + CLK_FREQ_MHZ=str(build["clock_mhz"]), + OPT_LEVEL=str(env["opt_level"]), + MAX_JOBS=str(args.tool_jobs)) + if build.get("impl_strategy"): + benv["IMPL_STRATEGY"] = build["impl_strategy"] + return ["make", "-C", work], benv + + def extras(self, work): + return {"critical_paths": read_paths(os.path.join(work, self.TIMING_RPT), + self.NUM_PATHS), + "high_fanout_nets": read_fanout(os.path.join(work, self.FANOUT_RPT), + self.NUM_FANOUT)} + + def incomplete(self, metrics): + return not (metrics.get("critical_paths") + and metrics.get("high_fanout_nets") is not None) + + def describe(self, m): + return "Fmax=%.1f MHz LUT=%d" % (m["fmax_mhz"], m["lut"]) + + def describe_baseline(self, base): + return "Fmax=%.0f MHz LUT=%d" % (base["fmax_mhz"], base["lut"]) + + +class Yosys(Tool): + """Yosys + OpenSTA on an ASIC PDK (hw/syn/yosys/dut). + + No licence and no dedicated machine, so unlike the Vivado gate this one is + cheap enough to run on a hosted runner. The metrics change with the target: + LUT/BRAM/DSP have no ASIC meaning, cell area and SRAM area do. + """ + + name = "yosys" + gate = "asic_gate" + flow = "hw/syn/yosys/dut" + + metrics = ("fmax_mhz", "wns_ns", "tns_ns", "cell_area_um2", "seq_area_um2", + "sram_area_um2", "cell_count", "power_mw", "build_time_s") + gated = ("fmax_mhz", "cell_area_um2") + integral = ("cell_count", "build_time_s") + columns = (("fmax", "fmax_mhz", "%.0f", 9), + ("area_um2", "cell_area_um2", "%.0f", 11), + ("sram_um2", "sram_area_um2", "%.0f", 10), + ("cells", "cell_count", "%d", 9), + ("power_mw", "power_mw", "%.1f", 9)) + + weights = {"vortex": 7200, "rtu": 11000, "gfx": 5400, "core": 3600, + "tcu": 3600, "tensor": 3600, "vm": 1800, "cache": 900, + "raster": 300, "dxa": 300, "tex": 300, "om": 120} + fallback_weight = 900 + job_label = "make" + + # sv2v is the parser: a bad define, a missing include or a hierarchy error + # dies there, before Yosys ever reads a line. run_synth.sh stamps `gen-ys` + # immediately after source generation and conversion have both succeeded, so + # that stamp is this flow's "past the typo risk" marker. + elab_done = "TIME gen-ys" + elab_label = "source conversion" + phases = ( + ("gen_sources.sh", "sources"), + ("sv2v.sh", "sv2v"), + (elab_done, "synthesis"), + ("TIME yosys", "sram"), + ("run_sta.tcl", "timing"), + ("DONE. Top:", "reporting"), + ) + error_re = re.compile(r"^(?:ERROR|FATAL|Error)\b|^\S+: \*\*\* ") + + def env(self, args): + return {"pdk": os.environ.get("PDK", "asap7"), + "vt": os.environ.get("ASAP7_VT", "rvt"), + "corner": os.environ.get("CORNER", "tt"), + "yosys": tool_version("yosys", "-V"), + "sta": tool_version("sta", "-version"), + "sv2v": tool_version("sv2v", "--version"), + "xlen": args.xlen} + + def hash_key(self, env): + # ASAP7 RVT-TT and LVT-SS are not comparable, so the PDK corner is part + # of the fingerprint the way the FPGA part number is for Vivado. + # + # The tool versions are in the fingerprint too, which they are NOT on the + # Vivado side: area and Fmax move with the Yosys/ABC release far more + # than Vivado's do, and often by more than the 5% gate tolerance. A + # toolchain bump must therefore read as STALE ("re-record"), not as a + # regression -- the alternative is a red gate that blames the RTL for a + # tool change. All three ship in one tarball pinned by TOOLCHAIN_REV, so + # this makes that pin part of the baseline contract. + return [env["pdk"], env["vt"], env["corner"], str(env["xlen"]), + env["yosys"], env["sta"], env["sv2v"]] + + def env_warning(self, build, env): + # Area and Fmax move with the Yosys/ABC version far more than Vivado's + # do, so a version change is worth saying out loud on every build. + for key, label in (("yosys", "Yosys"), ("sta", "OpenSTA"), + ("sv2v", "sv2v")): + ref = build.get("baseline_env", {}).get(key) + if ref and env.get(key) and ref != env[key]: + # The version is in config_hash, so this build is already going + # to read STALE. Say WHICH tool moved -- STALE alone cannot. + return ("baseline was recorded on %s %s, running %s -- results " + "across tool versions are not comparable, so this reads " + "as STALE until re-recorded" % (label, ref, env[key])) + return None + + def tops(self, dut_root): + """DUT -> top module, from dut/catalog.mk. The flow builds into + _, so the work directory is not knowable without it.""" + text = _catalog(dut_root) + return {d: _make_var(text, d + "_TOP") for d in _make_var(text, "DUTS").split()} + + def work_dir(self, dut_root, prefix, build): + top = self.tops(dut_root).get(build["dut"]) + if not top: + raise BuildError("no _TOP for '%s' in %s/catalog.mk" + % (build["dut"], dut_root)) + # hw/syn/yosys/Makefile builds in $(PREFIX)_$(TOP_LEVEL_ENTITY), one + # level above the DUT dispatcher. + return os.path.join(os.path.dirname(dut_root), "%s_%s" % (prefix, top)) + + def prepare_flow(self, dut_root, env): + """Install the PDK once, before the pool starts. + + The flow's `asap7` target is a prerequisite of every build and is phony, + so N parallel builds would otherwise race the same download into the + same directory. Installed once up front, they all hit its marker fast + path instead. + """ + subprocess.run(["make", "-C", os.path.dirname(dut_root), "asap7"], + stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, + check=True) + + def _make(self, dut_root, build, target): + return ["make", "-C", dut_root, build["dut"], "TARGET=" + target, + "PREFIX=%s_%s" % (self.gate, build["id"]), + "CLOCK_FREQ=%d" % build["clock_mhz"], + "CONFIGS=" + build["configs"]] + + def clean(self, dut_root, work, build, benv): + subprocess.run(self._make(dut_root, build, "clean"), env=benv, + stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, + check=True) + os.makedirs(work, exist_ok=True) + + def build_command(self, dut_root, work, build, env, args): + os.makedirs(work, exist_ok=True) + return self._make(dut_root, build, "timing"), {} + + def describe(self, m): + return "Fmax=%.1f MHz area=%.0f um^2" % (m.get("fmax_mhz") or 0, + m.get("cell_area_um2") or 0) + + def describe_baseline(self, base): + return "Fmax=%.0f MHz area=%.0f um^2" % (base["fmax_mhz"], + base.get("cell_area_um2") or 0) + + +TOOLS = {t.name: t for t in (Xilinx(), Yosys())} + + +def tool_version(name, flag): + """First line of ` `, or "" if it cannot be asked. + + Resolved out of $TOOLDIR the way hw/syn/common.mk does, with a PATH fallback: + the prebuilt toolchain is not on PATH, and a version recorded from some other + copy of yosys would make the baseline's tool env a lie. + """ + tooldir = os.environ.get("TOOLDIR") or os.path.expanduser("~/tools") + binary = os.path.join(tooldir, name, "bin", name) + if not os.path.exists(binary): + binary = name + try: + out = subprocess.run([binary, flag], stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, timeout=30) + except (OSError, subprocess.SubprocessError): + return "" + text = out.stdout.decode(errors="replace").strip() + if not text: + return "" + # Up to the build details: yosys reports its git sha AND the g++ that built + # it ("Yosys 0.57+157 (git sha1 ..., g++ 11.4 ...)"). Those identify the + # binary, not the tool, and this string is part of the baseline fingerprint + # -- rebuilding the same yosys must not invalidate every recorded number. + return text.splitlines()[0].split("(")[0].strip() + + +# --------------------------------------------------------------------------- +# catalog +# --------------------------------------------------------------------------- + +def source_root(): + """Source checkout root (has configure + VX_config.toml), not a build tree.""" + d = os.path.dirname(os.path.abspath(__file__)) + while d != "/": + if os.path.exists(os.path.join(d, "configure")) and \ + os.path.exists(os.path.join(d, "VX_config.toml")): + return d + d = os.path.dirname(d) + sys.exit("ERROR: not inside a Vortex source checkout") + + +def spec_path(root, gate): + return os.path.join(root, "ci", "testcases", gate + ".yaml") + + +def baseline_dir(root, tool): + return os.path.join(root, "ci", "baselines", "synthesis", tool) + + +def baseline_path(root, tool, group): + return os.path.join(baseline_dir(root, tool), group + ".json") + + +def load_catalog(root, tool): + """Builds keyed by id: the SPEC from ci/testcases/.yaml, joined to its + recorded metrics from ci/baselines/synthesis//.json. + + Same division as every other check in the catalog: the yaml says what to run + (and is hand-authored, commented, reviewed), the baseline says what the last + accepted answer was (and is machine-written). Neither file can be mistaken + for the other's job. + """ + with open(spec_path(root, tool.gate)) as fh: + doc = yaml.safe_load(fh) + defaults = doc.get("defaults") or {} + # `builds:`, not `tests:` -- a synthesis build is not a pytest case (no + # driver, no sim build tree, no xlen expansion, no app). The catalog's + # `tests:` block holds the ONE case that invokes this script; the build list + # is ours. + spec = doc.get("builds") + if not spec: + sys.exit("ERROR: no `builds:` block in %s" % spec_path(root, tool.gate)) + + baselines = {} + for group in {e.get("group", "default") for e in spec}: + path = baseline_path(root, tool.name, group) + if os.path.exists(path): + with open(path) as fh: + baselines[group] = json.load(fh) + + builds = {} + for entry in spec: + bid = entry["id"] + if bid in builds: + sys.exit("ERROR: duplicate build id '%s' in %s" + % (bid, spec_path(root, tool.gate))) + missing = {"dut", "clock_mhz", "configs"} - set(entry) + if missing: + sys.exit("ERROR: %s: missing %s" % (bid, ", ".join(sorted(missing)))) + group = entry.get("group", "default") + build = dict(entry) + build["group"] = group + build["configs"] = entry["configs"] or "" + build["known_issue"] = entry.get("known_issue", + defaults.get("known_issue", "")) + build["thresholds"] = entry.get("thresholds", + defaults.get("thresholds", {})) + recorded = baselines.get(group, {}).get(bid, {}) + result = recorded.get("result") or {} + # An all-null skeleton means "never recorded" -- the file ships with the + # shape visible, not with numbers. + build["baseline"] = result if result.get("fmax_mhz") is not None else None + build["baseline_env"] = result.get("env") or {} + build["config_hash"] = recorded.get("config_hash") + builds[bid] = build + return builds + + +def select(builds, selectors): + """Resolve --build selectors: an exact build id wins over a group of the + same name; otherwise a group selects all its builds.""" + if not selectors: + return [builds[b] for b in sorted(builds)] + picked = {} + for sel in selectors: + hits = [b for b in builds.values() if b["id"] == sel] + if not hits: + hits = [b for b in builds.values() if b["group"] == sel] + if not hits: + sys.exit("ERROR: no build or group named '%s' (see --list)" % sel) + for b in hits: + picked[b["id"]] = b + return [picked[b] for b in sorted(picked)] + + +def config_hash(build, env, tool): + """Fingerprint the synthesis inputs; a change invalidates the stored metrics.""" + key = "|".join([build["dut"], str(build["clock_mhz"]), build["configs"]] + + tool.hash_key(env)) + # Appended only when set, so builds without a strategy override keep the + # hash their baselines were recorded under. + if build.get("impl_strategy"): + key += "|" + build["impl_strategy"] + return hashlib.sha256(key.encode()).hexdigest()[:16] + + +def save_baselines(root, tool, results, env): + """Write measured metrics to the golden baselines (never done by CI). + + Each entry records the GPU configuration it represents -- dut, target clock, + and configs -- so a baseline is meaningful on its own: a set of metrics with + no identifiable config is worthless. The yaml `builds:` block stays the + source of truth for what to build; config_hash fingerprints that spec and + invalidates the metrics if it drifts, so the recorded config cannot be + hand-edited to disagree with the numbers next to it without failing the gate. + """ + by_group = {} + for r in results: + by_group.setdefault(r["build"]["group"], []).append(r) + os.makedirs(baseline_dir(root, tool.name), exist_ok=True) + for group, rs in by_group.items(): + path = baseline_path(root, tool.name, group) + entries = {} + if os.path.exists(path): + with open(path) as fh: + entries = json.load(fh) + for r in rs: + b = r["build"] + entries[b["id"]] = { + "dut": b["dut"], + "clock_mhz": b["clock_mhz"], + "configs": b["configs"], + "config_hash": config_hash(b, env, tool), + "result": dict(r["metrics"], env=dict(env)), + } + if b.get("impl_strategy"): + entries[b["id"]]["impl_strategy"] = b["impl_strategy"] + with open(path, "w") as fh: + json.dump(entries, fh, indent=2, sort_keys=True) + fh.write("\n") + print("updated %s" % path) + + +# --------------------------------------------------------------------------- +# build +# --------------------------------------------------------------------------- + +def prepare_build_tree(root, build_dir, xlen): + """Configure the gate's own out-of-tree build if it is not there yet.""" + if os.path.exists(os.path.join(build_dir, "config.mk")): + return + tooldir = os.environ.get("TOOLDIR") or os.path.expanduser("~/tools") + os.makedirs(build_dir, exist_ok=True) + print("configuring build tree %s (xlen=%d, tooldir=%s)" + % (build_dir, xlen, tooldir)) + subprocess.run([os.path.join(root, "configure"), + "--tooldir=%s" % tooldir, "--xlen=%d" % xlen], + cwd=build_dir, check=True) + + +def read_summary(path, tool): + """Parse the flow's synth_summary.csv into the metric dict. + + A metric the flow could not measure is written as an empty cell rather than + a zero (a missing SRAM estimate is not "no SRAM"), and stays absent here. + """ + with open(path) as fh: + row = next(csv.DictReader(fh)) + out = {} + for m in tool.metrics: + value = (row.get(m) or "").strip() + if not value: + continue + out[m] = int(float(value)) if m in tool.integral \ + else round(float(value), 3) + return out + + +def read_paths(path, limit): + """Top critical paths, parsed from Vivado's timing.rpt. + + project.tcl already writes one (`report_timing -unique_pins -nworst 100`), so + this needs no extra Vivado work and can be re-read from any finished build. + -unique_pins is what makes the list 10 REAL paths rather than 10 views of the + same one through different pins. + + The logic/route split is kept because it is the diagnosis, not decoration: a + path that is 66% route is placement/congestion, and no amount of retiming the + logic will fix it. + """ + if not os.path.exists(path): + return [] + paths, cur = [], None + with open(path, errors="replace") as fh: + for line in fh: + line = line.strip() + m = re.match(r"Slack \((MET|VIOLATED)\)\s*:\s*(-?[\d.]+)ns", line) + if m: + if cur: + paths.append(cur) + if len(paths) >= limit: + break + cur = {"slack_ns": float(m.group(2))} + elif cur is None: + continue + elif line.startswith("Source:"): + cur["startpoint"] = line.split(None, 1)[1] + elif line.startswith("Destination:"): + cur["endpoint"] = line.split(None, 1)[1] + elif line.startswith("Path Group:"): + cur["group"] = line.split(None, 2)[2] + elif line.startswith("Logic Levels:"): + cur["levels"] = int(line.split()[2]) + elif line.startswith("Data Path Delay:"): + d = re.search(r"logic ([\d.]+)ns .*?route ([\d.]+)ns \(([\d.]+)%\)", + line) + if d: + cur["logic_ns"] = float(d.group(1)) + cur["route_ns"] = float(d.group(2)) + cur["route_pct"] = float(d.group(3)) + if cur and len(paths) < limit: + paths.append(cur) + return paths + + +def read_fanout(path, limit): + """Top high-fanout nets, from the high_fanout_nets.rpt project.tcl writes. + + Recorded, never gated. A net fanning out to thousands of loads is what forces + the placer to spread its cone across the die, which is the routing delay the + critical paths then eat -- so this is the other half of the same diagnosis: + the paths say WHERE the design is tight, these say WHY. + """ + if not os.path.exists(path): + return [] + nets = [] + for line in open(path, errors="replace"): + cols = [c.strip() for c in line.split("|")[1:-1]] + if len(cols) != 3 or not cols[1].isdigit(): + continue # header, rule, or a non-table line + nets.append({"net": cols[0], "fanout": int(cols[1]), "driver": cols[2]}) + if len(nets) >= limit: + break + return nets + + +def threshold(build, metric, args): + """Gate tolerance for one metric: entry > per-metric global > global.""" + return float((build.get("thresholds") or {}).get( + metric, args.metric_threshold.get(metric, args.threshold))) + + +def say(build, tag, msg, t0=None): + at = "" if t0 is None else " (%dm)" % ((time.monotonic() - t0) // 60) + print(" [%-5s] %-10s %s%s" % (tag, build["id"], msg, at)) + + +def watch(proc, log, build, tool, timeout, verbose, seen=0): + """Follow a running build's log: report phases, announce elaboration, collect errors. + + A config mistake -- a bad define, a missing source, a parameter or hierarchy + error -- dies in the front end, seconds into the run. Passing the tool's + elab_done marker is therefore the point where a build stops being a typo risk + and starts being a long run worth waiting for, so it is announced as soon as + it lands rather than at the end. Vivado prints its marker on the way out of + elaboration whether it passed or failed, so a clean pass also requires no + elab_fail and no error behind it. + + Returns (rc, reached_elab, errors). + """ + reached = failed = False + errors, phase, t0, beat = [], None, time.monotonic(), time.monotonic() + while True: + rc = proc.poll() + if os.path.exists(log): + with open(log, errors="replace") as fh: + fh.seek(seen) + for line in fh: + if verbose: + print(" | %-10s %s" % (build["id"], line.rstrip())) + if tool.elab_fail and tool.elab_fail in line: + failed = True + elif tool.elab_done in line and not (failed or errors): + reached = True + say(build, "elab", "past %s -- into synthesis" + % tool.elab_label, t0) + for marker, label in tool.phases: + if marker in line and label != phase: + phase = label + if not verbose: # verbose already streams the line + say(build, "phase", label, t0) + if tool.error_re.match(line) and len(errors) < 5: + errors.append(line.strip()) + seen = fh.tell() + if rc is not None: + return rc, reached, errors + now = time.monotonic() + if now - t0 > timeout: + proc.kill() + raise BuildError("timed out after %ds (%s %s)" + % (timeout, tool.elab_label, + "passed" if reached else "NOT reached")) + if not verbose and now - beat >= HEARTBEAT: + beat = now + say(build, "...", "%s, still running" % (phase or "starting"), t0) + time.sleep(POLL_INTERVAL) + + +def load_stamp(work, chash, stamp_file): + """This build dir's resume state, if it belongs to the CURRENT config.""" + try: + with open(os.path.join(work, stamp_file)) as fh: + stamp = json.load(fh) + except (OSError, ValueError): + return None + # A stamp from a different config means the checkpoints in this tree are for + # a design we are no longer building -- resuming from them would silently + # synthesize the OLD config. Treat as absent: rebuild from scratch. + return stamp if stamp.get("config_hash") == chash else None + + +def save_stamp(work, chash, status, stamp_file, metrics=None): + with open(os.path.join(work, stamp_file), "w") as fh: + json.dump({"config_hash": chash, "status": status, "metrics": metrics}, + fh, indent=2, sort_keys=True) + fh.write("\n") + + +def run_build(build, build_dir, env, args, tool): + """Synthesize one DUT; return its metrics. Raises BuildError on failure.""" + dut_root = tool.dut_root(build_dir) + if build["dut"] not in dut_names(dut_root): + raise BuildError("no DUT target '%s' in %s/catalog.mk" + % (build["dut"], dut_root)) + + # Unique PREFIX per build id: its own build tree and its own log, so parallel + # builds (and other synthesis runs on this machine) cannot collide. It is + # also what keeps the Yosys flow's cached $(BUILD_DIR)/src from serving one + # DUT's sources to the next. + prefix = tool.gate + "_" + build["id"] + work = tool.work_dir(dut_root, prefix, build) + stamp_file = tool.gate + ".json" + chash = config_hash(build, env, tool) + + # Resume: a finished build of THIS config is reused as-is; an unfinished one + # keeps its build tree so the flow picks up from whatever it already + # produced instead of re-running hours of work. Anything else -- no stamp, + # or a stamp for a different config -- is a clean rebuild. + resume = False + if args.resume: + stamp = load_stamp(work, chash, stamp_file) + if stamp and stamp["status"] == "done" and stamp["metrics"]: + if not tool.incomplete(stamp["metrics"]): + say(build, "cache", "already built for this config -- reusing") + return stamp["metrics"] + say(build, "resum", "built, but its diagnostics predate this gate -- " + "re-reporting from checkpoint") + resume = True + elif stamp: + resume = True + say(build, "resum", "reusing build tree, resuming from checkpoint") + + benv = dict(os.environ) + argv, extra = tool.build_command(dut_root, work, build, env, args) + benv.update(extra) + if resume: + benv["RESUME"] = "1" # make clean is a no-op; keeps project_1 + *.dcp + + log = os.path.join(work, "build.log") + t0 = time.monotonic() + if not resume: + tool.clean(dut_root, work, build, benv) + # Stamp BEFORE building: a run killed mid-flight leaves a resumable marker. + save_stamp(work, chash, "running", stamp_file) + + if args.verbose: + say(build, "run", " ".join(argv)) + # On resume the log is appended, so start watching past what is already + # there -- otherwise the previous attempt's phases replay as if they were + # this one's (and its old ERROR: lines resurface as this run's failure). + seen = os.path.getsize(log) if resume and os.path.exists(log) else 0 + with open(log, "a" if resume else "w") as fh: + proc = subprocess.Popen(argv, env=benv, stdout=fh, + stderr=subprocess.STDOUT) + rc, reached_elab, errors = watch(proc, log, build, tool, args.timeout, + args.verbose, seen) + elapsed = int(time.monotonic() - t0) + + if rc != 0: + raise BuildError("%s (rc=%d) after %dm%s\n see %s" + % ("FAILED BEFORE SYNTHESIS -- config/RTL error" + if not reached_elab else "build failed", + rc, elapsed // 60, + "".join("\n | " + e for e in errors), log)) + + summary = os.path.join(work, "synth_summary.csv") + if not os.path.exists(summary): + raise BuildError("no synth_summary.csv (the flow did not reach report), " + "see %s" % log) + metrics = read_summary(summary, tool) + metrics["build_time_s"] = elapsed + metrics.update(tool.extras(work)) + save_stamp(work, chash, "done", stamp_file, metrics) + return metrics + + +def weight(build, tool): + """Scheduling cost: the recorded build time, else a per-DUT estimate.""" + recorded = (build.get("baseline") or {}).get("build_time_s") + return recorded or tool.weights.get(build["dut"], tool.fallback_weight) + + +def run_all(builds, build_dir, env, args, tool): + """Longest-processing-time-first list scheduling. + + Submitting longest-first to a fixed pool means a long build is always in + flight from t=0 (it is dispatched first) and the remaining slots churn + through the short ones -- so the makespan is bounded by the longest build + rather than by a short build starting last and running past everything. + """ + order = sorted(builds, key=lambda b: weight(b, tool), reverse=True) + print("schedule (%d builds, %d parallel, %d %s jobs each):" + % (len(order), args.jobs, args.tool_jobs, tool.job_label)) + for b in order: + print(" %-10s dut=%-8s %4d MHz ~%dm" + % (b["id"], b["dut"], b["clock_mhz"], weight(b, tool) // 60)) + if args.dry_run: + return [] + + tool.prepare_flow(tool.dut_root(build_dir), env) + + results = [] + with concurrent.futures.ThreadPoolExecutor(max_workers=args.jobs) as pool: + futures = {pool.submit(run_build, b, build_dir, env, args, tool): b + for b in order} + for fut in concurrent.futures.as_completed(futures): + b = futures[fut] + try: + metrics = fut.result() + say(b, "done", "%s (%dm)" % (tool.describe(metrics), + metrics["build_time_s"] // 60)) + results.append({"build": b, "metrics": metrics, "error": None}) + except (BuildError, subprocess.CalledProcessError) as e: + say(b, "FAIL", str(e)) + results.append({"build": b, "metrics": None, "error": str(e)}) + return sorted(results, key=lambda r: r["build"]["id"]) + + +# --------------------------------------------------------------------------- +# gate +# --------------------------------------------------------------------------- + +def gate(result, env, args, gated, tool): + """Compare one result against its baseline; return (verdict, [reasons]). + + A build carrying a `known_issue` reason is tracked-expected-failure: it still + builds, still reports, and its numbers still land in the table -- but its + verdict does not fail the run. Same contract as a `known_issue:` test case + (conftest marks those xfail strict=False), including that an unexpected pass + is surfaced (XPASS) rather than converted into a hard failure. + """ + build = result["build"] + known = build.get("known_issue") + + def verdict(name, reasons): + if not known: + return name, reasons + return "KNOWN-ISSUE", reasons + ["known issue: " + known] + + if result["error"]: + return verdict("BUILD-FAIL", [result["error"]]) + + base = build.get("baseline") or {} + if not any(base.get(m) is not None for m in gated): + return verdict("NO-BASELINE", + ["no recorded baseline (run --update-baseline)"]) + if build.get("config_hash") != config_hash(build, env, tool): + return verdict("STALE", ["config changed since the baseline was recorded " + "(re-record with --update-baseline)"]) + + reasons = [] + for m in gated: + cur, ref = result["metrics"].get(m), base.get(m) + if ref is None: + reasons.append("%s: missing from baseline" % m) + continue + if cur is None: + reasons.append("%s: not measured by this run" % m) + continue + tol = threshold(build, m, args) + delta = (cur - ref) / ref if ref else 0.0 + if abs(delta) <= tol: + continue + better = (delta > 0) == (m in HIGHER_IS_BETTER) + reasons.append("%s: %g -> %g (%+.1f%%, tolerance %.0f%%) -- %s" + % (m, ref, cur, 100 * delta, 100 * tol, + "improvement, update the baseline to lock it in" + if better else "REGRESSION")) + + # Target-frequency gate: Fmax must be within tolerance of the clock the design + # was BUILT for, independent of the baseline. A baseline recorded below target + # (a design that never met its clock) must not let a build pass just by matching + # that number -- the gate answers "does it meet the frequency it targets?", not + # only "did it get worse than last time?". + target = build.get("clock_mhz") + fmax = result["metrics"].get("fmax_mhz") + if target and fmax is not None: + tol = threshold(build, "fmax_mhz", args) + shortfall = (float(target) - fmax) / float(target) + if shortfall > tol: + reasons.append("fmax_mhz: %.1f MHz vs %g MHz target (%.1f%% below, " + "tolerance %.0f%%) -- BELOW TARGET" + % (fmax, target, 100 * shortfall, 100 * tol)) + + if not reasons: + return ("XPASS", ["known issue no longer reproduces -- clear " + "`known_issue` from the catalog"]) if known \ + else ("PASS", []) + return verdict("IMPROVED" if all("improvement" in r for r in reasons) + else "FAIL", reasons) + + +def report(results, env, args, gated, tool, record=False): + """Print the metric table; return the list of failing verdicts. + + In record mode the table is the review artifact for the baseline update, so + the per-metric deltas are still shown (that is the diff a human signs off on) + but they are not verdicts -- recording IS the intent. + """ + head = "%-10s %-8s" + "".join(" %%%ds" % c[3] for c in tool.columns) + title = head % (("build", "dut") + tuple(c[0] for c in tool.columns)) \ + + " %6s %s" % ("time", "verdict") + print("\n" + title) + # Slack in the rule: a cell carries its delta ("30113(+0%)") and routinely + # overruns its column, so the rows are wider than the header. + print("-" * (len(title) + 10)) + failures = [] + for r in results: + b = r["build"] + if record: + verdict, reasons = ("RECORDED", []) if r["metrics"] \ + else ("BUILD-FAIL", [r["error"]]) + else: + verdict, reasons = gate(r, env, args, gated, tool) + r["verdict"] = verdict + if reasons: # includes KNOWN-ISSUE/XPASS: reported, but not failing + failures.append((b["id"], verdict, reasons)) + if r["metrics"] is None: + print("%-10s %-8s %s" % (b["id"], b["dut"], verdict)) + continue + m, base = r["metrics"], b.get("baseline") or {} + + def cell(key, fmt): + cur = m.get(key) + if cur is None: + return "-" + ref = base.get(key) + if not ref: + return fmt % cur + return "%s(%+.0f%%)" % (fmt % cur, 100 * (cur - ref) / ref) + + print(head % ((b["id"], b["dut"]) + + tuple(cell(c[1], c[2]) for c in tool.columns)) + + " %5dm %s" % (m["build_time_s"] // 60, verdict)) + + for bid, verdict, reasons in failures: + print("\n%s: %s" % (bid, verdict)) + for reason in reasons: + print(" - %s" % reason) + return failures + + +# --------------------------------------------------------------------------- + +def main(argv=None, default_tool="xilinx"): + tool = TOOLS[default_tool] + ap = argparse.ArgumentParser( + description="Synthesis-regression gate (Fmax/area vs golden baseline)") + ap.add_argument("-b", "--build", action="append", default=[], metavar="ID", + help="build id or group to run (repeatable; default: all)") + ap.add_argument("-j", "--jobs", type=int, default=2, metavar="N", + help="max parallel synthesis builds (default: 2)") + ap.add_argument("--tool-jobs", "--vivado-jobs", type=int, default=0, + dest="tool_jobs", metavar="N", + help="tool jobs per build (default: cores/jobs, clamped to " + "%d..%d). Vivado only." % (MIN_TOOL_JOBS, MAX_TOOL_JOBS)) + ap.add_argument("--tool", default=default_tool, choices=sorted(TOOLS), + help="synthesis toolchain; selects the spec " + "ci/testcases/.yaml and the golden file tree " + "ci/baselines/synthesis// (default: %s)" + % default_tool) + ap.add_argument("--threshold", type=float, default=TOLERANCE, metavar="F", + help="global gate tolerance, fraction (default: %.2f). " + "Overridden per-metric by --metric-threshold and " + "per-build by the catalog's \"thresholds\"." + % TOLERANCE) + ap.add_argument("--metric-threshold", action="append", default=[], + metavar="M=F", + help="global tolerance for one metric, e.g. lut=0.10 " + "(repeatable); still overridden per-build") + ap.add_argument("--gate", metavar="M,M", + help="metrics to assert on (default: the tool's, %s for %s); " + "all metrics are always recorded and reported" + % (",".join(tool.gated), default_tool)) + ap.add_argument("--update-baseline", action="store_true", + help="record this run's metrics as the new golden baseline. " + "Human-reviewed; CI must never pass this.") + ap.add_argument("--build-dir", metavar="DIR", + help="build tree to synthesize in (default: " + "/build_, configured on demand)") + ap.add_argument("--xlen", type=int, default=32, choices=(32, 64), + help="ISA width (default: 32)") + ap.add_argument("--opt-level", type=int, default=3, metavar="N", + help="Vivado optimization level, 0=fastest 3=default") + ap.add_argument("--timeout", type=int, default=8 * 3600, metavar="SEC", + help="per-build timeout (default: 8h)") + ap.add_argument("--resume", action="store_true", + help="resume an interrupted session: builds already finished " + "for this config are reused, unfinished ones pick up " + "from their checkpoint, the rest run. A build whose " + "config changed is rebuilt clean.") + ap.add_argument("-v", "--verbose", action="store_true", + help="stream each build's tool log to stdout (prefixed by " + "build id) instead of phase/heartbeat lines") + ap.add_argument("--report", metavar="FILE", + help="write the full run (metrics + verdicts) as JSON") + ap.add_argument("--list", action="store_true", help="list builds and exit") + ap.add_argument("--matrix", action="store_true", + help="emit the selected builds as JSON and exit -- the " + "fan-out a workflow turns into one job per build") + ap.add_argument("--dry-run", action="store_true", + help="print the schedule without running the tool") + args = ap.parse_args(argv) + tool = TOOLS[args.tool] + + # Line-buffer stdout: progress is the point of the elaboration watch, and it + # is worthless if it sits in a block buffer until the run ends (which is what + # happens whenever this is piped -- CI, nohup, a log file). + sys.stdout.reconfigure(line_buffering=True) + + overrides = {} + for spec in args.metric_threshold: + metric, _, value = spec.partition("=") + if metric not in tool.metrics or not value: + sys.exit("ERROR: --metric-threshold expects =, " + "metric one of: %s" % ", ".join(tool.metrics)) + overrides[metric] = float(value) + args.metric_threshold = overrides + + root = source_root() + builds = load_catalog(root, tool) + + if args.matrix: + # Ordered longest-first so the slowest DUT is dispatched into the fan-out + # first; GitHub starts matrix jobs in order, so this is the same makespan + # argument run_all() makes for its thread pool. + print(json.dumps(sorted( + [{"id": b["id"], "group": b["group"], "dut": b["dut"], + "clock_mhz": b["clock_mhz"], + "known_issue": bool(b.get("known_issue"))} + for b in select(builds, args.build)], + key=lambda b: (-weight(builds[b["id"]], tool), b["id"])))) + return 0 + + if args.list: + for b in select(builds, args.build): + base = b.get("baseline") or {} + print("%-10s [%s] dut=%-8s %4d MHz %s%s" + % (b["id"], b["group"], b["dut"], b["clock_mhz"], + "baseline: " + tool.describe_baseline(base) + if base.get("fmax_mhz") else "no baseline", + " KNOWN ISSUE: " + b["known_issue"] + if b.get("known_issue") else "")) + print(" %s" % b["configs"]) + return 0 + + tool.check_env() + + if args.jobs < 1: + sys.exit("ERROR: --jobs must be >= 1") + if not args.tool_jobs: + args.tool_jobs = max(MIN_TOOL_JOBS, + min(MAX_TOOL_JOBS, os.cpu_count() // args.jobs)) + + build_dir = args.build_dir or os.path.join( + root, "build%d_%s" % (args.xlen, tool.gate)) + env = tool.env(args) + + selected = select(builds, args.build) + for b in selected: + warning = tool.env_warning(b, env) + if warning: + print("WARNING: %s %s" % (b["id"], warning)) + + if not args.dry_run: + prepare_build_tree(root, build_dir, args.xlen) + results = run_all(selected, build_dir, env, args, tool) + if args.dry_run: + return 0 + + gated = tuple(m for m in (args.gate.split(",") if args.gate else tool.gated) if m) + outcomes = report(results, env, args, gated, tool, args.update_baseline) + failures = [o for o in outcomes if o[1] not in PASSING] + + ok = [r for r in results if r["metrics"]] + if args.update_baseline and ok: + save_baselines(root, tool, ok, env) + + if args.report: + with open(args.report, "w") as fh: + json.dump({"tool": tool.name, "env": env, + "tolerance": args.threshold, + "metric_thresholds": args.metric_threshold, + "gated": list(gated), + "builds": [{"id": r["build"]["id"], + "group": r["build"]["group"], + "dut": r["build"]["dut"], + "clock_mhz": r["build"]["clock_mhz"], + "known_issue": r["build"].get("known_issue", ""), + "metrics": r["metrics"], + "error": r["error"], + "verdict": r["verdict"]} + for r in results]}, fh, indent=2) + fh.write("\n") + print("\nwrote %s" % args.report) + + + if args.update_baseline: + # Recording IS the intent; a delta is what we just wrote down. Only a + # build that never produced metrics is still a failure -- unless it is a + # tracked known issue. + broken = [r for r in results + if not r["metrics"] and not r["build"].get("known_issue")] + return 2 if broken else 0 + if any(v == "BUILD-FAIL" for _, v, _ in failures): + return 2 + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main()) From 62fd79aaf228f6e016dc6cefe7264a1dade7da5f Mon Sep 17 00:00:00 2001 From: Blaise Tine Date: Tue, 1 Sep 2026 17:48:23 -0700 Subject: [PATCH 09/10] ci: add asic_gate, the synthesis-regression gate over Yosys + ASAP7 The open-source counterpart of fpga_gate: the same golden/threshold/ ratchet discipline over sv2v -> Yosys -> ABC -> OpenSTA on ASAP7 (RVT, TT), gating post-synthesis Fmax and standard-cell area. Same DUTs as the Xilinx gate -- the hw/unittest wrappers, declared once in hw/syn/yosys/dut/catalog.mk -- so `make -C hw/syn/yosys/dut om` and the gate build the same thing, and a divergence between the two tools is a finding rather than an artefact of two DUT lists. Fmax does not mean on this flow what it means on Vivado. ABC maps to the target period and stops, so a DUT that closes does so with picoseconds of margin and its Fmax sits just above the clock it was built for by construction. Cell area is the metric that actually moves; the target-frequency check is the real Fmax assertion. Tool versions are part of config_hash, which they are not on the Vivado side: Yosys/ABC move area between releases by more than the 5% tolerance, so a toolchain bump must read as STALE ("re-record") rather than as a regression blaming the RTL. The strings are normalised to drop the git sha and the compiler that built the binary, so rebuilding the same yosys does not invalidate every number. CI: its own nightly workflow, not a ci.yml cell. A DUT is 1-2 hours, so eleven in one cell would be a day -- the builds fan out to one standalone hosted job each, fail-fast disabled so one regression cannot cancel the ten other measurements. Like fpga_gate.yml it pins master and skips itself when master has not moved, but a hosted runner keeps no state between runs, so the "already gated" marker is an actions-cache entry keyed by SHA and spec hash rather than a file in ~/.cache. synth_report.py joins the per-job reports into one summary table. Baselines recorded for the five characterised DUTs; the other six carry a known_issue naming what is missing, so the nightly builds and reports them without asserting numbers nobody has reviewed. The cross-check against the FPGA goldens is what says the memory inference is right: cache 89 BRAM / 16,307 um^2 SRAM, raster 10 / 3,728, tex 0 / 0. Co-Authored-By: Claude Opus 5 --- .github/workflows/asic_gate.yml | 243 +++++++++++++++++++++ ci/asic_gate.py | 39 ++++ ci/baselines/synthesis/yosys/core.json | 28 +++ ci/baselines/synthesis/yosys/dxa.json | 28 +++ ci/baselines/synthesis/yosys/graphics.json | 80 +++++++ ci/synth_report.py | 104 +++++++++ ci/testcase.py | 10 +- ci/testcases/asic_gate.yaml | 197 +++++++++++++++++ 8 files changed, 725 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/asic_gate.yml create mode 100755 ci/asic_gate.py create mode 100644 ci/baselines/synthesis/yosys/core.json create mode 100644 ci/baselines/synthesis/yosys/dxa.json create mode 100644 ci/baselines/synthesis/yosys/graphics.json create mode 100755 ci/synth_report.py create mode 100644 ci/testcases/asic_gate.yaml diff --git a/.github/workflows/asic_gate.yml b/.github/workflows/asic_gate.yml new file mode 100644 index 0000000000..16375d7f9b --- /dev/null +++ b/.github/workflows/asic_gate.yml @@ -0,0 +1,243 @@ +# Copyright © 2019-2026 +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# +# asic_gate — nightly ASIC synthesis-regression gate on hosted runners. +# Synthesizes the DUT catalog (ci/testcases/asic_gate.yaml) through +# Yosys + OpenSTA on ASAP7 and asserts Fmax/cell area against the golden results +# in ci/baselines/synthesis/yosys/, so an RTL change that costs timing closure or +# area cannot sit unnoticed on master. +# +# Why its own workflow rather than a cell in ci.yml: +# +# - A DUT takes 1-2 hours, so the builds must fan out to ONE STANDALONE JOB +# EACH. A ci.yml cell is one job running a pytest slice; eleven sequential +# synthesis runs in one cell would be a day. +# - Nightly builds are expensive even when free, so the run is SKIPPED unless +# master has actually moved since the last gate run. Unlike the self-hosted +# fpga_gate, a hosted runner keeps no state between runs, so the "already +# gated this commit" marker lives in the actions cache, keyed by SHA. +# +# It always gates master, whatever branch the schedule happens to fire on. No +# licence and no dedicated machine: yosys/sv2v/OpenSTA ship in the prebuilt +# toolchain and ASAP7 is a 59 MB content-addressed fetch. +# +# See docs/designs/continuous_integration.md §3.5. + +name: ASIC Gate + +on: + schedule: + - cron: '0 4 * * *' # nightly 04:00 UTC (an hour behind fpga_gate) + workflow_dispatch: + inputs: + builds: + description: "build ids/groups, space separated (blank = all)" + default: "" + force: + description: "run even if master has not moved since the last gate" + type: boolean + default: false + +# One sweep at a time, so two nights' runs cannot both claim the SHA marker. +# Never cancel a run in flight -- hours of synthesis are already spent. +concurrency: + group: asic-gate + cancel-in-progress: false + +env: + CCACHE_DISABLE: 1 + +jobs: + # --------------------------------------------------------------------------- + # plan — decide whether to run at all, and emit one matrix entry per build. + # No build env; just PyYAML. + # --------------------------------------------------------------------------- + plan: + runs-on: ubuntu-22.04 + outputs: + run: ${{ steps.q.outputs.run }} + builds: ${{ steps.q.outputs.builds }} + sha: ${{ steps.q.outputs.sha }} + key: ${{ steps.q.outputs.key }} + steps: + - uses: actions/checkout@v4 + with: + ref: master # hard-pinned: the gate always tracks master + fetch-depth: 0 + - run: pip install --quiet pyyaml + + # The marker key is master's head AS CHECKED OUT, not github.sha: on a + # schedule those are normally the same, but the thing being gated is what + # the checkout produced. Computed here rather than with hashFiles() in the + # step below, which cannot see a runtime value. + - name: Compute gate key + id: key + run: | + set -euo pipefail + SHA=$(git rev-parse HEAD) + # The spec is in the key too, so editing the build list re-gates a + # commit that was already gated under the old list. + SPEC=$(sha256sum ci/testcases/asic_gate.yaml | cut -c1-16) + echo "sha=$SHA" >> "$GITHUB_OUTPUT" + echo "key=asic-gate-$SHA-$SPEC" >> "$GITHUB_OUTPUT" + + # Probe (lookup-only, no download) for this commit's marker. A HIT means a + # previous run already reached a verdict on this SHA, so master has not + # moved and there is nothing new to gate. The `report` job writes the + # marker at the end. + - name: Read gate marker + id: marker + uses: actions/cache/restore@v4 + with: + path: .asic_gate_sha + key: ${{ steps.key.outputs.key }} + lookup-only: true + + - name: Plan builds + id: q + env: + IN_BUILDS: ${{ github.event.inputs.builds }} + FORCE: ${{ github.event.inputs.force }} + HIT: ${{ steps.marker.outputs.cache-hit }} + SHA: ${{ steps.key.outputs.sha }} + KEY: ${{ steps.key.outputs.key }} + run: | + set -euo pipefail + echo "sha=$SHA" >> "$GITHUB_OUTPUT" + echo "key=$KEY" >> "$GITHUB_OUTPUT" + if [ "$HIT" = "true" ] && [ "${FORCE:-}" != "true" ]; then + echo "master unchanged since the last gate run ($SHA) — skipping" + echo "run=false" >> "$GITHUB_OUTPUT" + echo "builds=[]" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "gating $SHA" + # Dispatch inputs only narrow the catalog, and reach the shell through + # an env var: interpolating one into the script would let a dispatch + # value execute arbitrary commands on the runner. + ARGS=() + for b in ${IN_BUILDS:-}; do ARGS+=(-b "$b"); done + BUILDS=$(python3 ci/asic_gate.py --matrix "${ARGS[@]}") + echo "builds=$BUILDS" >> "$GITHUB_OUTPUT" + [ "$BUILDS" = "[]" ] && echo "run=false" >> "$GITHUB_OUTPUT" || echo "run=true" >> "$GITHUB_OUTPUT" + python3 ci/testcase.py lint + + # --------------------------------------------------------------------------- + # synth — one standalone job per DUT. Each configures its own build tree and + # gates exactly one build, so a 2-hour DUT costs 2 hours of wall time, not 2 + # hours of everyone else's. + # --------------------------------------------------------------------------- + synth: + needs: plan + if: needs.plan.outputs.run == 'true' + runs-on: ubuntu-22.04 + timeout-minutes: 300 + strategy: + # Never cancel siblings: each DUT is an independent measurement, and one + # regression must not hide the ten other numbers this run would have + # produced. + fail-fast: false + max-parallel: 6 + matrix: + build: ${{ fromJson(needs.plan.outputs.builds) }} + steps: + # The SHA the plan job resolved, not `master`: master moving mid-run must + # not split one gate across two source trees. + - uses: actions/checkout@v4 + with: + ref: ${{ needs.plan.outputs.sha }} + submodules: recursive + + # Restores the prebuilt toolchain cache, which already carries yosys, sv2v + # and OpenSTA (ci/toolchain_install.sh installs all three by default). + - name: Setup Vortex + uses: ./.github/actions/setup-vortex + + # ASAP7 is content-addressed by its manifest, so that file's hash IS the + # cache key. Every job in the matrix shares it; the first to finish saves + # it and the rest log a harmless "cache already exists". The gate installs + # it on a miss (hw/syn/yosys/Makefile's `asap7` target) -- about a minute. + - name: Cache ASAP7 PDK + uses: actions/cache@v4 + with: + path: build32_asic_gate/hw/syn/libs/asap7 + key: asap7-rvt-${{ hashFiles('hw/syn/libs/asap7/manifest.txt') }} + + # The command is the one the catalog declares (ci/testcases/asic_gate.yaml's + # single `run:` case), narrowed to this job's build, so what CI runs and + # what the catalog says cannot drift apart. + - name: Gate ${{ matrix.build.id }} + id: run + env: + BUILD_ID: ${{ matrix.build.id }} + run: | + set -uo pipefail + CMD=$(python3 -c "import yaml; print(yaml.safe_load(open('ci/testcases/asic_gate.yaml'))['tests'][0]['run'])") + rc=0 + # --timeout under the job's timeout-minutes, so a hung build is + # reported BY the gate (with the phase it died in, and a report to + # upload) instead of vanishing into a GitHub job kill. + $CMD -b "$BUILD_ID" --timeout 16200 \ + --report "asic_gate_$BUILD_ID.json" || rc=$? + echo "rc=$rc" >> "$GITHUB_OUTPUT" + exit 0 + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: asic-gate-${{ matrix.build.id }} + path: | + asic_gate_${{ matrix.build.id }}.json + build32_asic_gate/hw/syn/yosys/asic_gate_*/build.log + build32_asic_gate/hw/syn/yosys/asic_gate_*/synth_summary.csv + build32_asic_gate/hw/syn/yosys/asic_gate_*/reports/ + if-no-files-found: warn + + - name: Verdict + run: | + case "${{ steps.run.outputs.rc }}" in + 0) echo "asic_gate ${{ matrix.build.id }} passed" ;; + 1) echo "::error::asic_gate ${{ matrix.build.id }} FAILED — Fmax/area moved beyond threshold vs baseline"; exit 1 ;; + *) echo "::error::asic_gate ${{ matrix.build.id }} build error (see logs)"; exit 1 ;; + esac + + # --------------------------------------------------------------------------- + # report — collect every build's verdict into one summary, and record the SHA + # so tomorrow's run skips an unchanged master. + # --------------------------------------------------------------------------- + report: + needs: [plan, synth] + if: always() && needs.plan.outputs.run == 'true' + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.plan.outputs.sha }} + - uses: actions/download-artifact@v4 + with: + pattern: asic-gate-* + path: reports + merge-multiple: true + + - name: Summarize + id: sum + run: | + set -uo pipefail + python3 ci/synth_report.py reports >> "$GITHUB_STEP_SUMMARY" + echo "rc=$?" >> "$GITHUB_OUTPUT" + exit 0 + + # Record the SHA once every build has reached a VERDICT (pass or + # regression), so a red master is not re-synthesized every night — the + # failed run is the record. A build error (rc=2) does NOT record, so the + # next nightly retries it. + - name: Record gated SHA + if: steps.sum.outputs.rc != '2' + run: echo "${{ needs.plan.outputs.sha }}" > .asic_gate_sha + - name: Save gate marker + if: steps.sum.outputs.rc != '2' + uses: actions/cache/save@v4 + with: + path: .asic_gate_sha + key: ${{ needs.plan.outputs.key }} diff --git a/ci/asic_gate.py b/ci/asic_gate.py new file mode 100755 index 0000000000..1a5dd579a2 --- /dev/null +++ b/ci/asic_gate.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""asic_gate — ASIC synthesis-regression gate (Yosys + OpenSTA, ASAP7). + +The open-source counterpart of ci/fpga_gate.py: the same golden-baseline +discipline, over a flow that needs no licence and no dedicated machine, gating +post-synthesis Fmax and standard-cell area. The gating machinery is shared and +lives in ci/synth_gate.py; this entry point only pins the tool. + + Spec : ci/testcases/asic_gate.yaml (what to build) + Baselines : ci/baselines/synthesis/yosys/.json (last accepted metrics) + Builds : hw/syn/yosys/asic_gate_ (out-of-tree, per DUT) + Metrics : /synth_summary.csv, written by hw/syn/yosys/synth_summary.py + from the reports/ set the flow already produces + +Unlike the FPGA gate this one runs on a hosted runner as a normal pytest cell +(`check: asic_gate`, tier `full`), because yosys/sv2v/OpenSTA ship in the +prebuilt toolchain and ASAP7 is a 59 MB fetch. + +Usage (from a source checkout): + ci/asic_gate.py --list + ci/asic_gate.py # all builds, gate against baselines + ci/asic_gate.py -b om -b tex -j 4 # explicit builds, 4 in parallel + ci/asic_gate.py -b graphics # a whole group + ci/asic_gate.py --update-baseline # re-record (human-reviewed, never in CI) + +The same DUTs are runnable by hand through the flow's own entry point -- +`make -C hw/syn/yosys/dut om` -- so the gate adds no second way to build. + +See docs/designs/continuous_integration.md 3.5. +""" + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import synth_gate # noqa: E402 + +if __name__ == "__main__": + sys.exit(synth_gate.main(default_tool="yosys")) diff --git a/ci/baselines/synthesis/yosys/core.json b/ci/baselines/synthesis/yosys/core.json new file mode 100644 index 0000000000..68c719be96 --- /dev/null +++ b/ci/baselines/synthesis/yosys/core.json @@ -0,0 +1,28 @@ +{ + "cache": { + "clock_mhz": 800, + "config_hash": "b511dc803abcf2a1", + "configs": "", + "dut": "cache", + "result": { + "build_time_s": 280, + "cell_area_um2": 47068.22, + "cell_count": 422936, + "env": { + "corner": "tt", + "pdk": "asap7", + "sta": "2.7.0", + "sv2v": "sv2v v0.0.13-3-g80a2f0c", + "vt": "rvt", + "xlen": 32, + "yosys": "Yosys 0.57+157" + }, + "fmax_mhz": 951.294, + "power_mw": 147.0, + "seq_area_um2": 22783.874, + "sram_area_um2": 16307.2, + "tns_ns": 0.0, + "wns_ns": 0.199 + } + } +} diff --git a/ci/baselines/synthesis/yosys/dxa.json b/ci/baselines/synthesis/yosys/dxa.json new file mode 100644 index 0000000000..310ecea816 --- /dev/null +++ b/ci/baselines/synthesis/yosys/dxa.json @@ -0,0 +1,28 @@ +{ + "dxa": { + "clock_mhz": 800, + "config_hash": "f7d52866f76ecc77", + "configs": "", + "dut": "dxa", + "result": { + "build_time_s": 115, + "cell_area_um2": 11719.171, + "cell_count": 107949, + "env": { + "corner": "tt", + "pdk": "asap7", + "sta": "2.7.0", + "sv2v": "sv2v v0.0.13-3-g80a2f0c", + "vt": "rvt", + "xlen": 32, + "yosys": "Yosys 0.57+157" + }, + "fmax_mhz": 883.314, + "power_mw": 36.2, + "seq_area_um2": 5196.02, + "sram_area_um2": 0.0, + "tns_ns": 0.0, + "wns_ns": 0.118 + } + } +} diff --git a/ci/baselines/synthesis/yosys/graphics.json b/ci/baselines/synthesis/yosys/graphics.json new file mode 100644 index 0000000000..37da29bde8 --- /dev/null +++ b/ci/baselines/synthesis/yosys/graphics.json @@ -0,0 +1,80 @@ +{ + "om": { + "clock_mhz": 800, + "config_hash": "5d5c754c246f3864", + "configs": "", + "dut": "om", + "result": { + "build_time_s": 65, + "cell_area_um2": 5233.054, + "cell_count": 46170, + "env": { + "corner": "tt", + "pdk": "asap7", + "sta": "2.7.0", + "sv2v": "sv2v v0.0.13-3-g80a2f0c", + "vt": "rvt", + "xlen": 32, + "yosys": "Yosys 0.57+157" + }, + "fmax_mhz": 862.292, + "power_mw": 40.2, + "seq_area_um2": 2499.304, + "sram_area_um2": 0.0, + "tns_ns": 0.0, + "wns_ns": 0.09 + } + }, + "raster": { + "clock_mhz": 800, + "config_hash": "1e66f6b8edb20977", + "configs": "", + "dut": "raster", + "result": { + "build_time_s": 95, + "cell_area_um2": 9973.332, + "cell_count": 93180, + "env": { + "corner": "tt", + "pdk": "asap7", + "sta": "2.7.0", + "sv2v": "sv2v v0.0.13-3-g80a2f0c", + "vt": "rvt", + "xlen": 32, + "yosys": "Yosys 0.57+157" + }, + "fmax_mhz": 809.585, + "power_mw": 93.0, + "seq_area_um2": 3953.513, + "sram_area_um2": 3728.0, + "tns_ns": 0.0, + "wns_ns": 0.015 + } + }, + "tex": { + "clock_mhz": 800, + "config_hash": "a45d6ce5711405fd", + "configs": "", + "dut": "tex", + "result": { + "build_time_s": 1085, + "cell_area_um2": 112430.725, + "cell_count": 1089160, + "env": { + "corner": "tt", + "pdk": "asap7", + "sta": "2.7.0", + "sv2v": "sv2v v0.0.13-3-g80a2f0c", + "vt": "rvt", + "xlen": 32, + "yosys": "Yosys 0.57+157" + }, + "fmax_mhz": 804.959, + "power_mw": 614.0, + "seq_area_um2": 37780.862, + "sram_area_um2": 0.0, + "tns_ns": 0.0, + "wns_ns": 0.008 + } + } +} diff --git a/ci/synth_report.py b/ci/synth_report.py new file mode 100755 index 0000000000..b59ba5c234 --- /dev/null +++ b/ci/synth_report.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""synth_report — collapse a fan-out of synthesis-gate reports into one summary. + +.github/workflows/asic_gate.yml runs one standalone job per DUT, so the run's +verdict is scattered across N `--report` JSONs. This joins them into a single +Markdown table for the workflow summary, and exits with the worst outcome so the +caller can decide whether the commit was actually gated: + + 0 every build passed (or is a tracked known issue) + 1 at least one build regressed + 2 at least one build never produced metrics -- the commit is NOT gated + + usage: synth_report.py [...] +""" + +import glob +import json +import os +import sys + +# Same contract as synth_gate.PASSING: a tracked known issue and an unexpected +# pass are reported, not failed. +PASSING = ("PASS", "RECORDED", "KNOWN-ISSUE", "XPASS") + +# Shown in this order when a build reports them; the rest of the metrics stay in +# the JSON artifacts, which is where a real investigation goes anyway. +COLUMNS = (("Fmax (MHz)", "fmax_mhz", "%.1f"), + ("Area (um^2)", "cell_area_um2", "%.0f"), + ("SRAM (um^2)", "sram_area_um2", "%.0f"), + ("Cells", "cell_count", "%d"), + ("LUT", "lut", "%d"), + ("FF", "ff", "%d")) + + +def load(paths): + """Every build across every report, newest-wins on a duplicate id.""" + builds, env, tool = {}, {}, "" + for path in paths: + try: + with open(path) as fh: + doc = json.load(fh) + except (OSError, ValueError) as e: + print("WARNING: %s: %s" % (path, e), file=sys.stderr) + continue + env = doc.get("env") or env + tool = doc.get("tool") or tool + for b in doc.get("builds", []): + builds[b["id"]] = b + return builds, env, tool + + +def main(argv=None): + argv = list(sys.argv[1:] if argv is None else argv) + if not argv: + sys.exit(__doc__.strip().splitlines()[-1].strip()) + paths = [] + for arg in argv: + paths += sorted(glob.glob(os.path.join(arg, "**", "*.json"), + recursive=True)) if os.path.isdir(arg) else [arg] + builds, env, tool = load(paths) + if not builds: + print("## synthesis gate\n\nNo reports found — every build errored " + "before it could write one.") + return 2 + + # Only the columns some build actually measured: an FPGA run has no cell + # area and an ASIC run has no LUTs, and empty columns are noise. + cols = [c for c in COLUMNS + if any((b.get("metrics") or {}).get(c[1]) is not None + for b in builds.values())] + + print("## %s gate\n" % (tool or "synthesis")) + if env: + print("`%s`\n" % " ".join("%s=%s" % kv for kv in sorted(env.items()))) + print("| build | dut | " + " | ".join(c[0] for c in cols) + + " | time | verdict |") + print("|---|---|" + "---|" * (len(cols) + 2)) + + worst = 0 + for bid in sorted(builds): + b = builds[bid] + m = b.get("metrics") or {} + cells = [] + for _, key, fmt in cols: + v = m.get(key) + cells.append("—" if v is None else fmt % v) + secs = m.get("build_time_s") + verdict = b.get("verdict") or "?" + note = " — %s" % b["known_issue"] if b.get("known_issue") else "" + print("| %s | %s | %s | %s | %s%s |" + % (bid, b.get("dut", ""), " | ".join(cells), + "—" if secs is None else "%dm" % (secs // 60), verdict, note)) + if verdict not in PASSING: + worst = max(worst, 2 if verdict == "BUILD-FAIL" else 1) + + if worst: + print("\n%d of %d builds did not pass." + % (sum(1 for b in builds.values() + if (b.get("verdict") or "?") not in PASSING), len(builds))) + return worst + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/ci/testcase.py b/ci/testcase.py index 958cc18cfa..7bd506c109 100644 --- a/ci/testcase.py +++ b/ci/testcase.py @@ -41,10 +41,12 @@ VALID_CHECK = {"model_parity", "perf_gate"} # Tiers that must be asked for BY NAME. An empty --tier means "everything" (the -# nightly), and everything is what a hosted runner can run -- these cannot be: -# `fpga` needs a licensed Vivado and hours of a whole machine, and runs only on -# the self-hosted runner's own workflow. Opting in is explicit, never implied. -OPT_IN_TIERS = {"fpga"} +# nightly), and everything is what ci.yml's matrix can run as a CELL -- these +# cannot be. `fpga` needs a licensed Vivado and hours of a whole machine; +# `asic` needs 1-2 hours PER DUT, which is a fan-out of standalone jobs, not one +# cell. Each has its own workflow (.github/workflows/{fpga,asic}_gate.yml). +# Opting in is explicit, never implied. +OPT_IN_TIERS = {"fpga", "asic"} # simx<->rtlsim cycle-parity default: both timing models must agree within 5%. DEFAULT_PARITY_TOLERANCE = 0.05 diff --git a/ci/testcases/asic_gate.yaml b/ci/testcases/asic_gate.yaml new file mode 100644 index 0000000000..1c195aabdd --- /dev/null +++ b/ci/testcases/asic_gate.yaml @@ -0,0 +1,197 @@ +# ASIC synthesis-regression gate: post-synthesis Fmax/cell area must not regress +# past a threshold vs. a checked-in golden result +# (ci/baselines/synthesis/yosys/.json). +# +# Sibling of the fpga_gate (fpga_gate.yaml): same discipline — golden baseline, +# hard threshold, ratcheted improvements, results only ever re-recorded by a +# human — over the open-source flow instead of Vivado. What changes is the +# target: LUT/BRAM/DSP have no ASIC meaning, standard-cell area and SRAM area do. +# +# --------------------------------------------------------------------------- +# Two blocks, two consumers, exactly as in fpga_gate.yaml: +# +# tests: what pytest/CI sees — ONE case, whose `run:` is literally the +# command CI executes. No second entry point, no divergence. +# builds: what ci/asic_gate.py reads — the build list, with its per-build +# `known_issue` and `thresholds`. The pytest loader ignores this key. +# +# UNLIKE fpga_gate, THIS ONE RUNS ON A HOSTED RUNNER. Yosys, sv2v and OpenSTA +# ship in the prebuilt toolchain (ci/toolchain_install.sh installs all three by +# default) and ASAP7 is a 59 MB content-addressed fetch, so there is no licence +# and no dedicated machine in the way — which is the whole point of having an +# open-source flow. +# +# It is still nightly, not a PR gate: a DUT takes 1-2 hours. Tier `asic` is +# OPT-IN (OPT_IN_TIERS in testcase.py), so ci.yml's hosted matrix never emits a +# cell for it; .github/workflows/asic_gate.yml owns it, fanning the `builds:` +# block below out to ONE JOB PER BUILD so the wall times run concurrently +# instead of in series. +# +# The DUTs are the same hw/unittest/*/VX_*_top.sv wrappers the Xilinx gate +# synthesizes (hw/syn/yosys/dut/catalog.mk holds each one's top, include path +# and defines), so both gates measure the same modules and a divergence between +# them is a real finding rather than an artefact of two DUT lists. +# +# See docs/designs/continuous_integration.md §3.5. +category: asic_gate +defaults: + # 32-bit only: synthesis results are recorded against RV32. + xlen: + - 32 + tier: asic + via: script + # Any RTL change can move Fmax or area. + touches: + - hw/rtl/ +tests: +# The base command. asic_gate.yml reads it from here and appends `-b ` for +# the one build that job owns, so what CI runs and what the catalog says cannot +# drift apart. Run bare, it gates every build in series. +# +# `-j 1`: the workflow gets its parallelism from the job fan-out, and one Yosys +# build already saturates a hosted runner's two cores. +- id: gate + run: ci/asic_gate.py -j 1 + +# --------------------------------------------------------------------------- +# builds — read by ci/asic_gate.py, ignored by the pytest catalog loader. +# +# dut hw/syn/yosys/dut/catalog.mk target to synthesize; that catalog +# carries the DUT's top module, include path and defines +# clock_mhz target clock the design is constrained to, via CLOCK_FREQ. Fmax +# is derived from it and the worst slack, and the gate also asserts +# the design MEETS it — so this is a commitment, not a hint. +# configs EXTRA defines, appended to the catalog's. Usually empty: the DUT +# configuration belongs in one place, and that place is catalog.mk. +# group which golden file the results land in (mirrors the Xilinx groups) +# thresholds per-metric gate tolerance, overriding --metric-threshold and +# --threshold (e.g. {cell_area_um2: 0.10}) +# known_issue tracked expected failure: still builds and reports, does not fail +# the run (same contract as a known_issue: test case) +# +# Run one by name: ci/asic_gate.py -b tcu +# Run a group: ci/asic_gate.py -b graphics +# By hand, no gate: make -C hw/syn/yosys/dut tcu +# Re-record: ci/asic_gate.py --update-baseline (human-reviewed, never CI) +# +# PDK: ASAP7 7.5-track, RVT, TT corner (hw/syn/yosys/Makefile defaults). The +# corner is part of config_hash — RVT-TT and LVT-SS numbers are not comparable. +# --------------------------------------------------------------------------- +builds: + +# --- core: the base pipeline and its memory system --------------------------- + +# L1/LLC datapath with atomics: tag/data pipeline, MSHR, AMO unit. +- id: cache + group: core + dut: cache + clock_mhz: 800 + configs: "" + +# Wide-SIMD core: issue/operand-collect/LSU/FPU at NT=NW=16, plus the RVC +# decompressor and atomics. +- id: core + group: core + dut: core + clock_mhz: 500 + configs: "" + known_issue: >- + no baseline recorded yet, so its numbers are reported but not asserted. The + nightly job still builds it — read the run's metrics, sanity-check + clock_mhz, then record with `ci/asic_gate.py -b core --update-baseline`. + +# --- dxa --------------------------------------------------------------------- + +# DXA engine: descriptor queue, address generation, multicast distribution. +- id: dxa + group: dxa + dut: dxa + clock_mhz: 800 + configs: "" + +# --- tensor ------------------------------------------------------------------ + +# TFR tensor core with every datatype plus sparsity and WGMMA. The widest +# datapath in the tree, so it is the area + Fmax bellwether. +- id: tcu + group: tensor + dut: tcu + clock_mhz: 500 + configs: "" + known_issue: >- + no baseline recorded yet, so its numbers are reported but not asserted. The + nightly job still builds it — read the run's metrics, sanity-check + clock_mhz, then record with `ci/asic_gate.py -b tcu --update-baseline`. + +# Full tensor subsystem in one wrapper: TCU (WGMMA + MX) + dxa behind a shared +# L2, 2 cores / socket. +- id: tensor + group: tensor + dut: tensor + clock_mhz: 500 + configs: "" + known_issue: >- + no baseline recorded yet, so its numbers are reported but not asserted. The + nightly job still builds it — read the run's metrics, sanity-check + clock_mhz, then record with `ci/asic_gate.py -b tensor --update-baseline`. + +# --- graphics ---------------------------------------------------------------- + +# Rasterizer: bin/block/quad walker, edge evaluators, rcache port. +- id: raster + group: graphics + dut: raster + clock_mhz: 800 + configs: "" + +# Output merger: depth/stencil test, blend, ocache port. +- id: om + group: graphics + dut: om + clock_mhz: 800 + configs: "" + +# Texture unit: address generation, LOD, bilinear filter, tcache port. +- id: tex + group: graphics + dut: tex + clock_mhz: 800 + configs: "" + +# Ray-tracing unit: CW-BVH4 walker, box/triangle PEs, short stack. +- id: rtu + group: graphics + dut: rtu + clock_mhz: 400 + configs: "" + known_issue: >- + no baseline recorded yet. An earlier run showed its RTCache inferred to + flip-flops (sram_area_um2 = 0 against 1.2M cells) — that was the dispatcher + dropping -DASIC, since fixed, but the DUT has not been re-measured since. + Confirm sram_area_um2 is non-zero before recording. + +# Full graphics subsystem in one wrapper: raster + om + tex + rtu + dxa behind a +# shared L2, 2 cores / socket. +- id: gfx + group: graphics + dut: gfx + clock_mhz: 400 + configs: "" + known_issue: >- + no baseline recorded yet, so its numbers are reported but not asserted. The + nightly job still builds it — read the run's metrics, sanity-check + clock_mhz, then record with `ci/asic_gate.py -b gfx --update-baseline`. + +# --- vm ---------------------------------------------------------------------- + +# Standalone MMU network: dTLB + iTLB, pooled L1 TLB MSHR, L2 TLB, PTW walker +# hierarchy, fed by a shared L2. +- id: vm + group: vm + dut: vm + clock_mhz: 500 + configs: "" + known_issue: >- + no baseline recorded yet, so its numbers are reported but not asserted. The + nightly job still builds it — read the run's metrics, sanity-check + clock_mhz, then record with `ci/asic_gate.py -b vm --update-baseline`. From c716b1842d0f250b900bdfa23c4cec009368a48a Mon Sep 17 00:00:00 2001 From: Blaise Tine Date: Tue, 1 Sep 2026 17:48:29 -0700 Subject: [PATCH 10/10] docs: document asic_gate and the per-DUT ASIC flow continuous_integration.md 3.5 covers both gates and what a Tool adapter supplies; 4.5 is the new fan-out workflow. synthesis_analysis.md gains the dut/ dispatcher and the gate, plus the two things a reader needs before trusting an Fmax from this flow: ABC closes to the target, and report_wns clamps at zero. AGENTS.md gets the synthesis baselines under the same never-hand-edit rule as the perf ones, and the note that the two gates measure the same modules -- so the DUT catalogs must stay in step. Co-Authored-By: Claude Opus 5 --- AGENTS.md | 1 + CHANGELOG.md | 2 + docs/codebase.md | 2 + docs/designs/continuous_integration.md | 166 +++++++++++++++++++------ docs/synthesis_analysis.md | 33 ++++- 5 files changed, 165 insertions(+), 39 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7edbcd3393..e74c942e25 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -87,6 +87,7 @@ See [docs/testing.md](docs/testing.md) and [docs/debugging.md](docs/debugging.md - **RTL coverage path is `xrt`, not `rtlsim`.** When discussing or planning RTL verification, `xrt` is the canonical path — `rtlsim` bypasses the AFU surface. `rtlsim` remains useful for fast iteration on processor RTL; `xrt` is what proves the full integration. - **`ci/regression.sh` is the canonical source of tested configurations.** Use it to discover supported parameter combinations before inventing ad hoc ones. - **Perf-regression baselines (`ci/baselines/perf/*.json`) are golden data — never hand-edit them, and never "fix" a red perf gate by bumping the number.** They are regenerated only by `pytest ci -m perf_gate --update-baselines` (a human-run, reviewed step), and CI must never pass that flag. A `perf_gate` failure means real cycles moved: root-cause it, or — if the change is intended — regenerate the baseline so the diff shows the perf delta for review. Same rule as image goldens and `known_issue:`. +- **Synthesis baselines (`ci/baselines/synthesis/{xilinx,yosys}/*.json`) are golden data under the same rule** — never hand-edited, regenerated only by a human running `ci/fpga_gate.py --update-baseline` / `ci/asic_gate.py --update-baseline`, never by CI. A red `fpga_gate`/`asic_gate` means real Fmax or area moved. Note the two gates measure the *same* `hw/unittest/*/VX_*_top.sv` wrappers through different flows, so a divergence between them is a finding, not noise — keep `hw/syn/{xilinx,yosys}/dut/catalog.mk` in step when you add or rename a DUT. See [docs/designs/continuous_integration.md §3.5](docs/designs/continuous_integration.md). - **SimX is the RTL's timing model — keep them in lockstep.** Any change that moves RTL cycles (pipeline structure, arbitration, queue depths, cache/memory behavior) must land together with the matching SimX timing-model update, and vice-versa. The `model_parity` CI gate enforces this: a `check: model_parity` case (`ci/testcases/core.yaml` + per-extension parity categories) runs the same app/args/configs on simx and rtlsim and asserts exact retired-instruction match plus cycle agreement within the case tolerance (default 5%). Never widen a tolerance to absorb a divergence — model the behavior. When adding a hardware feature, add or extend a parity case that exercises it. - **When RTL debugging stalls, switch to the SimX-as-oracle pattern.** For numerical bugs, deep pipeline races, or any failure mode where rtlsim is "close but wrong": (1) build/extend the SimX C++ model so it mirrors the *new* RTL architecture and gets to PASS; (2) add matching trace dumps to both SimX and RTL (cycle, FU events, SRAM addresses+data, hazards) — same CSV format on both sides; (3) diff trace files — the first divergence is the bug. Don't keep guessing from output values; localize via trace diff. See [docs/debugging.md](docs/debugging.md#simx-as-oracle-for-rtl-debug). diff --git a/CHANGELOG.md b/CHANGELOG.md index e263f8e5cc..0cd94dcadc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,8 @@ The 3.0 release introduces a fixed-function graphics stack (rasterizer, texture - **Performance-regression gate (`perf_gate`).** Catalog cases marked `check: perf_gate` run on rtlsim and gate against goldens in [ci/baselines/perf/](ci/baselines/perf/): retired instructions must match the baseline **exactly** (a mismatch means the workload changed, not performance), cycles must stay within ±2%, and a config hash invalidates a baseline whose run configuration drifted. The ratchet is two-sided — an improvement beyond 2% also fails until a human locks it in with `pytest ci --update-baselines` — so the goldens always state the real number. Coverage spans core, dxa, graphics, raytracing, and the tensor/tensor_sp/tensor_wg/tensor_mx families. *Why:* cycle regressions surface at the change that caused them instead of in a later benchmark sweep. - **SimX↔RTL model-parity gate (`model_parity`).** Catalog cases marked `check: model_parity` run the same app/args/configs on **both** drivers and assert (1) identical retired-instruction counts — a mismatch is functional divergence, not a timing gap — and (2) a cycle gap within the case's tolerance. SimX is the RTL's timing model; this check keeps the two in lockstep. Known modelling gaps carry an explicit `known_issue:` reason in the catalog rather than silently passing. - **FPGA synthesis-regression gate (`fpga_gate`).** [ci/fpga_gate.py](ci/fpga_gate.py) puts nine DUTs (cache, core, top, dxa, tcu, rtu, raster, om, tex) through Vivado synthesis + place-and-route at their target clocks and gates post-implementation Fmax and area (LUT/FF/LUTRAM/BRAM/URAM/DSP) against goldens in [ci/baselines/synthesis/xilinx/](ci/baselines/synthesis/xilinx/) (default ±5%, per-build/per-metric overrides in [ci/testcases/fpga_gate.yaml](ci/testcases/fpga_gate.yaml)). Longest-first 2-up scheduling, per-build resume-from-checkpoint stamps, and an RTL-elaboration watch that fails a config typo in minutes instead of hours. The `fpga` tier is opt-in: it runs only on the self-hosted Vivado runner (`.github/workflows/fpga_gate.yml`) or by hand, never on hosted CI; goldens are re-recorded only by a human via `--update-baseline`. *Why:* the same golden/threshold/ratchet discipline as `perf_gate`, applied to what synthesis costs (timing closure, area) rather than what execution costs (cycles). +- **ASIC synthesis-regression gate (`asic_gate`).** The open-source counterpart of the FPGA gate: [ci/asic_gate.py](ci/asic_gate.py) puts the same `hw/unittest` DUT wrappers through sv2v → Yosys → ABC → OpenSTA on ASAP7 (RVT, TT) at their target clocks and gates post-synthesis **Fmax** and **standard-cell area** against goldens in [ci/baselines/synthesis/yosys/](ci/baselines/synthesis/yosys/), recording WNS/TNS, sequential and SRAM area, cell count and power alongside. The gating machinery is now shared: [ci/synth_gate.py](ci/synth_gate.py) holds the catalog, `config_hash`, thresholds, `known_issue`, resumable sessions, scheduler and report, and `fpga_gate.py`/`asic_gate.py` are entry points that pin a tool adapter — `fpga_gate` behaviour is unchanged, config hashes included. DUTs are declared once per flow in `hw/syn/{xilinx,yosys}/dut/catalog.mk`, so `make -C hw/syn/yosys/dut om` and the gate run the same thing. The `asic` tier is opt-in and the gate runs from [.github/workflows/asic_gate.yml](.github/workflows/asic_gate.yml) — nightly, skipped when master has not moved, and **fanned out to one hosted job per DUT** because each takes 1-2 hours. *Why:* the flow needs no licence and no dedicated machine, so the discipline the `fpga_gate` applies on a self-hosted Vivado box can cover every contributor's changes. Two flows over the same modules also make a divergence between them a finding rather than noise. + Three flow fixes fell out of building it. `run_sta.tcl` now reports `report_worst_slack` (signed) instead of deriving Fmax from `report_wns`, which clamps at zero and made every design that closed report exactly its target clock. `configure` now copies **every** `*.mk` fragment into a build tree, not just `common.mk`, so an included `catalog.mk`/`build.mk` resolves from `build/` as well as from the source tree. And each synthesis flow's mandatory defines are now `override CONFIGS +=`: a `CONFIGS=` on a **make command line** silently defeats every `CONFIGS +=` in a makefile, so the DUT dispatcher was dropping `-DSYNTHESIS -DASIC -DYOSYS` — the cache DUT synthesized to 1.08 M cells of flip-flop-inferred RAM with zero SRAM area. The Xilinx and Altera DUT flows carried the same trap (their gate passes `CONFIGS` through the environment, which appends rather than overrides, which is why it never fired there) and are hardened the same way; a command-line `CONFIGS` is now extra defines on top of the flow's, which is how it reads. - **tests/opencl: full Rodinia suite + atomics benchmarks.** The OpenCL test tree now carries the Rodinia benchmark suite wired into CI — backprop, bfs, b+tree, cfd, dwt2d, gaussian, heartwall, hotspot, hotspot3D, hybridsort, kmeans, lavaMD, lud, myocyte, nn (nearn), nw, pathfinder, srad, and streamcluster — plus atomics benchmarks (`atomicreduce`, `histogram`, in both `tests/opencl/` and `tests/hip/`) that exercise the `A`-extension end-to-end across the L1/L2/L3 hierarchy, and the OpenCL image benchmark set (`image_*`; see the OpenCL image-support entry above). *Why:* moves OpenCL coverage from a handful of kernels to a standard, externally comparable workload suite that stresses the caches, atomics, and the image path together. - **Hardware virtual memory (MMU v2).** RTL translation hierarchy ([hw/rtl/vm/](hw/rtl/vm/)): per-L1-cache socket TLB stage (non-blocking dTLB/iTLB + miss station), shared cluster L2 TLB (512e 4-way + megapage array), 2-walker PTW with walk cache; DCR-sourced satp, kill+report faults, page-granular allocation; `vm` CI on simx+rtlsim at XLEN 32/64. See [docs/designs/virtual_memory_subsystem.md](docs/designs/virtual_memory_subsystem.md). - **Multi-threaded SimX.** Per-socket execution domains on lockstep worker threads (`-DSIMX_MT=` via build `CONFIGS`), with cycles bit-identical across every thread count. New port-only communication API: `SimEventLink` control-plane links, KMU push dispatch matching the RTL launch bus, and framework-owned quiescence via `SimPlatform::idle()`. diff --git a/docs/codebase.md b/docs/codebase.md index 0e6f84cf53..e355ad3045 100644 --- a/docs/codebase.md +++ b/docs/codebase.md @@ -9,6 +9,8 @@ The directory/file layout of the Vortex codebase is as follows: - `testcases`: declarative test catalog (YAML), executed by the pytest harness - `baselines/perf`: golden performance baselines for the perf-regression gate - `baselines/synthesis/xilinx`: golden FPGA synthesis baselines for the fpga_gate + - `baselines/synthesis/yosys`: golden ASIC synthesis baselines for the asic_gate + - `synth_gate.py`: synthesis-regression gate; `fpga_gate.py`/`asic_gate.py` pin its tool - `blackbox.sh`: universal application launcher for all drivers - `regression.sh`: local entry point into the CI catalog - `docs`: documentation diff --git a/docs/designs/continuous_integration.md b/docs/designs/continuous_integration.md index 37c1fcec92..c4fe54e463 100644 --- a/docs/designs/continuous_integration.md +++ b/docs/designs/continuous_integration.md @@ -243,7 +243,7 @@ the threshold only absorbs benign, intended micro-changes. are separate cases: perf ids stay bare (`sgemm` — the golden baseline is keyed by it, `core:sgemm:rtlsim`), parity twins are prefixed `parity-`. -### 3.5 Synthesis-regression checks (`fpga_gate`) +### 3.5 Synthesis-regression gates (`fpga_gate`, `asic_gate`) The perf_gate catches a change that costs *cycles*. The fpga_gate catches one that costs *timing closure or area*: it synthesizes a catalog of DUTs with @@ -252,11 +252,42 @@ checked-in golden baseline within ±5% (`ci/fpga_gate.py`). Same discipline as §3.4 — regression fails, an unlocked improvement also fails and asks you to record it, and CI never writes a baseline. -It is **not** a pytest cell. A cell is a build tree plus a driver; an fpga_gate -build is an hours-long Vivado run that only exists on a host with the licensed -toolchain, so it is a standalone script driven by its own workflow on the -self-hosted runner (§4.4). `asic_gate` — the same script over -`ci/baselines/synthesis/yosys/` — is the planned second tool. +`asic_gate` is the same gate over the open-source flow: Yosys + OpenSTA on +ASAP7, asserting post-synthesis **Fmax** and **standard-cell area** against +`ci/baselines/synthesis/yosys/` (`ci/asic_gate.py`, §4.5). + +Neither is a pytest cell. A cell is a build tree plus a driver; a synthesis build +is neither, and both gates are hours long — Vivado because it is Vivado, Yosys +because a DUT is 1–2 hours. They are standalone scripts driven by their own +workflows (§4.4, §4.5), and their tiers (`fpga`, `asic`) are opt-in so `ci.yml`'s +hosted matrix never emits a cell for them. + +**One implementation, two adapters.** `ci/synth_gate.py` holds everything that is +not tool-specific — catalog loading, `config_hash`, threshold resolution, +`known_issue`, the resumable session, the scheduler, the gate itself and the +report. A `Tool` supplies only what genuinely differs: the metric names, how the +flow is invoked, what its log looks like, and which environment fields make two +runs comparable. `ci/fpga_gate.py` and `ci/asic_gate.py` are entry points that +pin `--tool`; everything below applies to both unless it names one. + +| | `fpga_gate` (xilinx) | `asic_gate` (yosys) | +|---|---|---| +| flow | Vivado synth + place-and-route | sv2v → Yosys → ABC → OpenSTA | +| DUT tree | `hw/syn/xilinx/dut` | `hw/syn/yosys/dut` | +| gated | `fmax_mhz`, `lut` | `fmax_mhz`, `cell_area_um2` | +| also recorded | `wns_ns`, `lutram`, `ff`, `bram`, `uram`, `dsp`, critical paths, high-fanout nets | `wns_ns`, `tns_ns`, `seq_area_um2`, `sram_area_um2`, `cell_count`, `power_mw` | +| comparable when | same device, opt level, xlen | same PDK, VT, corner, xlen | +| runner | self-hosted, licensed Vivado | hosted, one job per DUT | + +**Fmax means something different on the two.** Vivado's placer-and-router +reports what the implemented design achieved. ABC maps to the *target* period and +stops, so a Yosys DUT that closes does so with picoseconds of margin +(`om`: +0.032 ns on a 1.25 ns period) and its Fmax sits just above the clock it +was built for by construction. That makes **cell area the sensitive metric** on +the ASIC side, and Fmax mostly a met/missed signal — which is exactly what the +target-frequency check below asserts. It also makes the slack source matter: +`report_wns` is worst *negative* slack and clamps at zero, so `run_sta.tcl` uses +`report_worst_slack`, which is signed. - **Spec and baseline are split**, exactly as everywhere else in the catalog. The spec is `ci/testcases/fpga_gate.yaml` — hand-authored, commented, @@ -266,21 +297,29 @@ self-hosted runner (§4.4). `asic_gate` — the same script over hand-edited, carrying only measured metrics plus the config fingerprint and tool env they were measured under. Groups: `core` (cache+AMO, wide core, full 4-core AFU), `tensor` (all-datatype TCU), `graphics` (RTU/RASTER/OM/TEX), - `dxa`. A `config_hash` (dut/clock/configs/device/opt-level/xlen) ties the two - together — edit the spec and the gate refuses to compare against numbers - recorded for the old one (`STALE`). -- **Tier `fpga` is opt-in.** An empty `--tier` means "everything", and - everything is what a *hosted* runner can run — which these cannot: they need a - licensed Vivado and hours of a whole machine. `OPT_IN_TIERS` in `testcase.py` - keeps them out of every hosted event (including the nightly) unless asked for - by name, so they run only on the self-hosted runner's own workflow (§4.4). -- **Metrics** all come from `synth_summary.csv`, which `hw/syn/xilinx/dut/ - project.tcl` already emits post-implementation: Fmax, WNS, LUT, LUTRAM, FF, - BRAM, URAM, DSP — plus the wall-clock build time the runner measures. Every - metric is recorded and reported; `--gate` picks which ones are *asserted* - (Fmax and LUT by default). Build time is bookkeeping, not a gate: it is too - host-dependent to assert, and it is what the scheduler orders the queue by. -- **Critical paths**: each build also records its **top 10 unique critical + `dxa`; `asic_gate` mirrors them so a divergence between the two tools on the + same module is a real finding. A `config_hash` ties the two files together — + edit the spec and the gate refuses to compare against numbers recorded for the + old one (`STALE`). Its tool-specific half is the FPGA part plus opt level for + Vivado, and the PDK/VT/corner for Yosys, since ASAP7 RVT-TT and LVT-SS numbers + are not comparable any more than two different FPGAs are. +- **Tiers `fpga` and `asic` are opt-in.** An empty `--tier` means "everything", + and everything is what a `ci.yml` *cell* can run — which these cannot. `fpga` + needs a licensed Vivado and hours of a whole machine; `asic` needs no licence + at all but 1–2 hours **per DUT**, which is a fan-out of standalone jobs rather + than one cell. `OPT_IN_TIERS` in `testcase.py` keeps both out of every hosted + event (including the nightly) unless asked for by name, so each runs only from + its own workflow (§4.4, §4.5). +- **Metrics** all come from one `synth_summary.csv` per build, so the gate needs + no per-report parser and format knowledge stays next to the tool that produces + it. `hw/syn/xilinx/dut/project.tcl` writes it post-implementation; + `hw/syn/yosys/synth_summary.py` writes the ASIC equivalent by collapsing the + `reports/{stat_lib,sram_area,worst_slack,tns,power}.rpt` set the flow already + emits. Every metric is recorded and reported; `--gate` picks which ones are + *asserted*. Build time is bookkeeping, not a gate: it is too host-dependent to + assert, and it is what the scheduler orders the queue by. +- **Critical paths** (Vivado only — Yosys/OpenSTA writes no equivalent report + the gate reads): each build also records its **top 10 unique critical paths** (slack, logic levels, clock group, startpoint, endpoint) — emitted by `project.tcl` whether or not timing closed, because a design that *meets* its target still has a worst path, and watching where it sits across commits is @@ -299,35 +338,55 @@ self-hosted runner (§4.4). `asic_gate` — the same script over which stops reproducing surfaces as **XPASS** — reported loudly, asking you to clear the flag, but not converted into a hard failure. - **Early-failure watch**: a config mistake — a bad define, a missing source, a - parameter or hierarchy error — kills a build during *RTL elaboration*, seconds - into an otherwise multi-hour Vivado run. The runner follows each build's log - live and announces `Finished RTL Elaboration` as it lands, so a typo surfaces - in minutes instead of at the end of the sweep; a build that dies before that - point is reported as `FAILED BEFORE SYNTHESIS` with its Vivado `ERROR:` lines + parameter or hierarchy error — kills a build in the front end, seconds into an + otherwise multi-hour run. The runner follows each build's log live and + announces the point past which that can no longer happen: `Finished RTL + Elaboration` for Vivado, and for Yosys the `TIME gen-ys` stamp, which lands + once source generation *and* sv2v conversion have both succeeded — sv2v is the + parser on that flow, so it is where a bad define dies. A build that fails + before that point is reported as `FAILED BEFORE SYNTHESIS` with its error lines quoted inline, not as a generic non-zero make. - **Resumable sessions**: a sweep is hours long, so an interrupted one is picked back up rather than restarted. Each build dir carries a stamp - (`fpga_gate.json`: config hash + status + metrics), so `--resume` reuses a + (`.json`: config hash + status + metrics), so `--resume` reuses a build already finished *for this config*, lets an unfinished one pick up from - its Vivado post-synth/post-impl checkpoint, and runs the rest. A build whose + whatever it already produced (Vivado's post-synth/post-impl checkpoint; make's + own dependency graph on the Yosys side), and runs the rest. A build whose config changed since its stamp is rebuilt clean — resuming from those checkpoints would silently re-synthesize the old design. State lives next to the build tree it describes, not in a central session file, so it survives a kill and never desynchronizes from what is on disk. -- **Progress**: each build reports Vivado phase transitions (setup → - elaboration → synthesis → opt → placement → routing → reporting) as they - land, with a heartbeat in between; `-v` streams the raw Vivado log instead. +- **Progress**: each build reports its flow's phase transitions as they land + (Vivado: setup → elaboration → synthesis → opt → placement → routing → + reporting; Yosys: sources → sv2v → synthesis → sram → timing → reporting), + with a heartbeat in between; `-v` streams the raw tool log instead. - **Scheduling** is longest-processing-time-first over the recorded build times: the longest build is dispatched first so it is in flight from t=0, and the remaining slots churn through the short ones behind it. `-j` caps parallel builds (2 on the runner) and each build's Vivado job count is derived from it so the machine is not oversubscribed. Every build gets a unique `PREFIX` - (`fpga_gate_`), so it has its own build tree and log and cannot collide - with a parallel build or with a hand-run synthesis on the same machine. -- **Updating** — `ci/fpga_gate.py --update-baseline`, human-reviewed, committed - as an explicit `Fmax: 312 → 287` diff. Baselines also record the Vivado - version they were measured on; a run under a different version warns, because - Fmax across tool versions is not comparable. + (`_`), so it has its own build tree and log and cannot collide + with a parallel build or with a hand-run synthesis on the same machine. On the + Yosys flow that is load-bearing rather than hygienic: `hw/syn/yosys/Makefile` + caches `$(BUILD_DIR)/src` and does not regenerate it when `EXTRA_INCLUDE` + changes, so two DUTs sharing one tree would silently synthesize the first + one's sources. +- **Target-frequency check**: Fmax must also be within tolerance of the clock + the design was *built* for, independent of the baseline. A baseline recorded + below target must not let a build pass just by matching it — the gate answers + "does it meet the frequency it targets?", not only "did it get worse than last + time?". On the ASIC side this is the primary Fmax assertion, for the ABC reason + above. +- **Updating** — `ci/{fpga,asic}_gate.py --update-baseline`, human-reviewed, + committed as an explicit `Fmax: 312 → 287` diff. Baselines also record the tool + versions they were measured on (Vivado; Yosys/OpenSTA/sv2v), and a run under a + different version warns — results across tool versions are not comparable, and + Yosys/ABC move area and Fmax far more between releases than Vivado does. +- **DUT catalogs.** Each flow declares its DUTs once, in `dut/catalog.mk`, and + both a human (`make -C hw/syn/yosys/dut om`) and the gate go through it — so + the command CI runs and the command a developer runs cannot diverge. The gate + reads `DUTS` from that file to reject an unknown `dut:` in the spec before + starting a build. --- @@ -401,6 +460,39 @@ regression — so a red master is not re-synthesized every night (the failed run is the record); an infra/build error does not record, so the next nightly retries it. +### 4.5 `asic_gate.yml` — nightly synthesis, fanned out on hosted runners + +The ASIC gate needs no licence and no dedicated machine, so it runs on stock +hosted runners. What it does need is *time*: 1–2 hours **per DUT**. That is the +shape that keeps it out of `ci.yml` — a cell is one job running a pytest slice, +and eleven synthesis runs in series inside one cell would be a day. + +So it is its own workflow, and its builds **fan out to one standalone job each**: + +- `plan` reads `ci/testcases/asic_gate.yaml` via `ci/asic_gate.py --matrix` and + emits one matrix entry per build, ordered longest-first (GitHub starts matrix + jobs in order, so the slowest DUT is dispatched first — the same makespan + argument the in-process scheduler makes). +- `synth` runs one build per job with `fail-fast: false`. Each DUT is an + independent measurement, and one regression must not cancel the ten other + numbers the run would have produced. The job's command comes from the + catalog's single `run:` case, narrowed with `-b `, so what CI runs and what + the catalog declares cannot drift apart. +- `report` joins the per-job `--report` JSONs into one table + (`ci/synth_report.py`) in the workflow summary, and records the SHA marker. + +Like `fpga_gate.yml` it **hard-pins master** and **skips itself when master has +not moved** — but a hosted runner keeps no state between runs, so the "already +gated this commit" marker is an actions-cache entry keyed by SHA (plus the spec's +hash, so editing the build list re-gates) rather than a file in `~/.cache`. The +marker is written once every build has reached a *verdict*, on the same reasoning +as §4.4: a red master is not re-synthesized every night, and a build error does +not record so the next nightly retries it. + +The prebuilt toolchain already carries yosys, sv2v and OpenSTA, so `setup-vortex` +needs no change. ASAP7 is content-addressed by `hw/syn/libs/asap7/manifest.txt`, +so that file's hash is the PDK cache key; the flow installs it on a miss. + --- ## 5. Migration — done diff --git a/docs/synthesis_analysis.md b/docs/synthesis_analysis.md index 0170fc3e76..b2bb361da5 100644 --- a/docs/synthesis_analysis.md +++ b/docs/synthesis_analysis.md @@ -436,7 +436,34 @@ Build directory: `_/` (e.g., `test_Vortex/`). The flow uses `sv2v` to convert SystemVerilog sources to Verilog before feeding them to Yosys. -The first ASAP7 invocation runs the generated `build/ci/asap7_install.sh` installer. It downloads and SHA-256 verifies only the selected VT's five logical groups (`INVBUF`, `SIMPLE`, `AO`, `OA`, and `SEQ`) at TT, SS, and FF, then writes the merged Liberty files to `build/hw/syn/libs/asap7/lib/`. Gate-level simulation additionally installs the selected functional Verilog models in `build/hw/syn/libs/asap7/verilog/`. Later invocations verify the installed files and skip the download and preparation. The pin manifest, installer, preparation script, and upstream license are versioned in `ci/`; no ASAP7 collateral is kept under `third_party/`, and the flow requires no OpenROAD executable or physical collateral. +#### Per-DUT synthesis + +The DUTs above are declared once, in `hw/syn/yosys/dut/catalog.mk` — a top module, an include path and a define set each — and driven through one dispatcher, so a hand run and the `asic_gate` (below) build the same thing: + +```bash +make -C build/hw/syn/yosys/dut list # what is available +make -C build/hw/syn/yosys/dut om # synthesis + STA (TARGET=timing) +make -C build/hw/syn/yosys/dut tcu TARGET=synthesis # synthesis only +make -C build/hw/syn/yosys/dut om CLOCK_FREQ=500 # override the target clock +``` + +`PREFIX` defaults to the DUT name, so each DUT gets its own tree. That is not cosmetic: the flow caches `$(BUILD_DIR)/src` and does **not** regenerate it when `EXTRA_INCLUDE` changes, so two DUTs sharing one tree silently synthesize the first one's sources. Override `PREFIX` to keep an automated sweep away from a hand-run build of the same DUT. + +#### Synthesis-regression gate (`asic_gate`) + +`ci/asic_gate.py` runs that catalog against checked-in goldens in `ci/baselines/synthesis/yosys/` and fails on a Fmax or cell-area move beyond ±5%. It is the ASIC sibling of `ci/fpga_gate.py` and shares its implementation (`ci/synth_gate.py`). + +```bash +ci/asic_gate.py --list # builds and their recorded baselines +ci/asic_gate.py -b om -b tex # gate two builds +ci/asic_gate.py --update-baseline # re-record (human-reviewed, never in CI) +``` + +Two things about Fmax on this flow are worth knowing before reading a number. ABC maps to the *target* period and stops, so a design that closes does so with picoseconds of margin and its Fmax sits just above `CLOCK_FREQ` by construction — **cell area is the sensitive metric**, and Fmax is mostly a met/missed signal. And `report_wns` is worst *negative* slack, clamped at zero: `run_sta.tcl` uses `report_worst_slack` (signed) for exactly this reason, which is what `worst_slack.rpt` holds. + +See [docs/designs/continuous_integration.md](designs/continuous_integration.md) §3.5 and §4.5. + +The first ASAP7 invocation runs the generated `build/hw/syn/libs/asap7/install.sh` installer. It downloads and SHA-256 verifies only the selected VT's five logical groups (`INVBUF`, `SIMPLE`, `AO`, `OA`, and `SEQ`) at TT, SS, and FF, then writes the merged Liberty files to `build/hw/syn/libs/asap7/lib/`. Gate-level simulation additionally installs the selected functional Verilog models in `build/hw/syn/libs/asap7/verilog/`. Later invocations verify the installed files and skip the download and preparation. The pin manifest, installer, preparation script, and upstream license are versioned in `hw/syn/libs/asap7/`, alongside the other standard-cell collateral; `configure` instantiates only `install.sh.in` into the build tree, and it reads the manifest and preparation script from the source tree in place. No ASAP7 collateral is kept under `third_party/`, and the flow requires no OpenROAD executable or physical collateral. ### Threshold-Voltage Selection @@ -493,7 +520,9 @@ All reports are under `/reports/`: | `sram_area.rpt` | Estimated SRAM area breakdown | | `sta.log` | OpenSTA timing log | | `setup.rpt` / `hold.rpt` | Detailed setup and hold paths | -| `wns.rpt` / `tns.rpt` | Worst and total negative slack | +| `wns.rpt` / `tns.rpt` | Worst and total negative slack (clamped at 0 when timing closes) | +| `worst_slack.rpt` | Worst slack, **signed** — positive when the design closes with margin | +| `synth_summary.csv` | One-row machine-readable summary of all of the above (`synth_summary.py`) | | `power.rpt` | Power estimate (vectorless or SAIF-annotated) | | `power_hier.rpt` | Hierarchical power breakdown | | `saif_annotated.rpt` | Pins covered by SAIF |