⚠️ This repository is under active development. Interfaces and build flows may change.
CachePool is a Snitch–Spatz–based many-core system with a shared L1 data cache ("CachePool") and DRAMSys-backed main memory. Configuration is centralized in config/config.mk and propagated automatically to:
- SystemVerilog (via
VLOG_DEFSat compile time) - The Spatz cluster generator (via an auto-generated
config/cachepool.hjson)
| Level | Module | Description |
|---|---|---|
| 1 | Core Complex (CC) | One 32-bit Snitch + one Spatz RVV accelerator |
| 2 | Tile | 4 CCs + 4 × InSitu-Cache banks |
| 3 | Group | 4 Tiles connected via crossbar + shared L2 ICache |
| 4 | Cluster | Multiple Groups connected via a FlooNoC XY mesh (L1 request/response) plus a separate TCDM-based L2 refill mesh |
All tiles across all groups share one unified L1 data cache, interleaved across cache banks. The bank-selection offset is configurable at runtime via l1d_xbar_config(...).
For NumGroups > 1, two independent FlooNoC-based meshes connect the groups:
- L1 group mesh (
cachepool_group_noc_wrapper.sv): XY-routed mesh offloo_routerinstances carrying inter-group cache-line requests/responses between tiles. - L2 refill mesh (
cachepool_cluster.sv,gen_l2_refill_mesh): a separate torus-like mesh (distinct flit/header types) that routes DRAM refill traffic from each group tofloo_tcdm_chimneyedge nodes, which bridge into the DRAMSys/HBM channels.
Single-group configurations bypass both meshes; the intra-group crossbar path is unchanged.
- Linux environment with:
make,git,python3,curl - CMake ≥ 3.28, GCC/G++ ≥ 11.2
- QuestaSim (tested with
questa-2023.4) - Optional: SpyGlass for lint
This repository uses Bender to manage dependencies and generate simulation scripts. Ensure Bender is installed, or build it locally:
make benderBuild the RISC-V toolchains (LLVM + GCC). Spike (riscv-isa-sim) is also available through a dedicated target:
make toolchainFor ETH users, a pre-built toolchain is available for faster setup. Source iis-env.sh
to point the toolchain at it and set up the Python venv needed for RTL/config generation:
# ETH only: point at a prebuilt toolchain and set up the venv
source iis-env.shUse Bender to initialize all required submodules:
make initDRAMSys must be compiled before simulation. Tool versions can be overridden inline:
make dram-build CMAKE=/path/to/cmake-3.28.x CC=/path/to/gcc-11.2 CXX=/path/to/g++-11.2Some RTL components (e.g., package headers) must be generated prior to simulation.
Generation requires specifying a configuration. If none is provided, the default is cachepool_fpu_4g.
make generate config=cachepool_fpu_4gmake generate now also regenerates the FlooNoC package (update-floonoc) as a prerequisite, so the correct mesh topology for the selected config is always built automatically. Run make update-floonoc config=<name> standalone if you only need to refresh the NoC package (e.g. after editing a config/floonoc_*.yml topology file).
The BootROM is built separately from the RTL generation step:
make bootrom config=cachepool_fpu_4gHardware and software builds are decoupled: make sw builds software only, make vsim builds hardware only. Build whichever you need (or both) independently.
make sw config=cachepool_fpu_4gmake vsim config=cachepool_fpu_4gSet DEBUG=0 to disable +acc waveform visibility and speed up simulation (used by CI); default is DEBUG=1.
The wrapper script launches the simulation (GUI or CLI) and expects a software ELF path as argument:
# GUI mode
./sim/bin/cachepool_cluster.vsim.gui ./software/build/TESTNAME
# Headless mode
./sim/bin/cachepool_cluster.vsim ./software/build/TESTNAMEA lightweight benchmarking automation flow is provided under util/auto-benchmark to simplify batch testing of multiple configurations and kernels.
| File | Description |
|---|---|
configs.sh |
Defines configurations (CONFIGS) and kernel suffixes (KERNELS) to test, along with optional PREFIX and ROOT_PATH. |
run_all.sh |
Builds each configuration with DEBUG=0, runs all kernels, saves logs, generates summaries, and runs check-ci.py on the results. |
write_results.py |
Extracts [UART] lines from simulator logs and appends them to per-configuration summary files. |
check-ci.py |
Scans a simulation log for failures and exits non-zero if any are found (see CI Checking). |
-
Edit
configs.shto list the desired configurations and kernels:CONFIGS="cachepool_fpu_4g cachepool_fpu_16g" KERNELS="fdotp-32b_M32768 ffft-64b_M16384 fmatmul-64b_M2048" PREFIX="test-cachepool-" ROOT_PATH=../.. -
Run all builds and simulations:
./run_all.sh -
Results will appear in:
logs/<timestamp>/and a symlink:
logs/latest -> logs/<timestamp>/
Example directory after a run:
logs/20251028-1230/
├── cachepool_fpu_4g_fdotp-32b_M32768.log
├── cachepool_fpu_4g_fdotp-32b_M32768_pm/
├── cachepool_fpu_4g_summary.txt
├── cachepool_fpu_16g_summary.txt
└── ...
Each run includes:
*.log— Full simulation output*_pm/— Performance monitor logs automatically moved fromsim/bin/logsand renamed to<config>_<kernel>_pm/*_summary.txt—[UART]summaries for each configuration, grouped by kernel with clear headers
This setup allows quick reproducible benchmarks with all results neatly organized per run.
check-ci.py scans a simulation log and exits non-zero if any failure is detected, making it suitable for integration into CI pipelines. It flags the following patterns:
- Any line containing
FAILor[FAIL](case-insensitive) - Any line matching
error <N>where N is non-zero (error 0is treated as pass)
Usage:
python3 check-ci.py logs/latest/cachepool_fpu_4g_load-store.logExit code 0 means all tests passed; exit code 1 means at least one failure was detected. On failure the offending lines and their line numbers are printed for manual inspection.
GitLab CI (.gitlab-ci.yml) uses this same flow directly: a build stage runs make clean generate update-floonoc bootrom vsim sw config=$CI_CONFIG DEBUG=0 (default CI_CONFIG=cachepool_fpu_4g), then a test stage runs each kernel in parallel and pipes its log through check-ci.py.
All hardware knobs live in config/config.mk (and flavor files it includes). The default configuration is cachepool_fpu_4g: 4 groups (2×2 mesh), 4 tiles/group, 4 cores/tile = 64 cores total. 2-group configurations have been removed — they are no longer compatible with the L2 mesh interconnect.
Configuration names encode the number of groups and whether the FPU is enabled:
| Name | Groups | Mesh | FPU | Tiles/group | Cores/tile | Cores |
|---|---|---|---|---|---|---|
cachepool_4g |
4 | 2×2 | No | 4 | 4 | 64 |
cachepool_fpu_4g |
4 | 2×2 | Yes | 4 | 4 | 64 |
cachepool_fpu_16g |
16 | 4×4 | Yes | 4 | 4 | 256 |
cachepool_fpu_16g_tiny |
16 | 4×4 | Yes | 2 | 2 | 64 |
cachepool_fpu_16g_tiny shrinks tiles/group and cores/tile for a faster-to-build, faster-to-simulate smoke test of the full 16-group mesh topology.
The Spatz cluster consumes config/cachepool.hjson, which is generated from:
config/cachepool.hjson.tmpl(skeleton with comments)config/config.mk(source of truth)
Multi-group configurations also require a FlooNoC topology file (e.g. config/floonoc_cachepool_4g.yml, config/floonoc_cachepool_16g.yml, config/floonoc_cachepool_16g_tiny.yml), auto-selected by the config name suffix. make generate regenerates the FlooNoC package automatically; run make update-floonoc standalone only if you need to refresh it without a full generate.
To switch configurations, always clean first:
make clean
make generate config=cachepool_fpu_4gconfig/config.mkdefines all parameters (e.g.,num_groups,num_groups_x,num_tiles_per_group,num_cores_per_tile,l1d_cacheline_width,axi_user_width, etc.). Derived values are pre-computed so tools receive integers, not expressions.make generatecalls the Python generator to produceconfig/cachepool.hjsonfrom the template.- The Makefile passes the same values to QuestaSim via
VLOG_DEFS, keeping RTL, sim, and HJSON in sync.
- DRAMSys: multi-channel main memory with compile-time interleaving. The interleave granularity (bytes) is determined by the DRAM beat width and an
Interleavefactor in RTL. This is fixed at elaboration and not configurable at runtime. - L1D cache banking: runtime-configurable crossbar bit selection allows distributing core traffic across banks for parallelism. Use
l1d_xbar_config(...)at runtime to choose the offset.
L1 cache banks can be partitioned at runtime between a shared pool (accessible cluster-wide via the interconnect) and a private partition (local to each tile). Five modes are supported:
| Mode | l1d_part value |
Private banks | Shared banks |
|---|---|---|---|
| All-shared | 0 | none | all |
| 1 private, 3 shared | 1 | 1 | 3 |
| Half-half | 2 | 2 | 2 |
| 3 private, 1 shared | 3 | 3 | 1 |
| All-private | 4 | all | none |
Private banks are local to each tile and not visible to remote tiles. Shared banks participate in the cluster-wide interleaved pool. For non-power-of-2 partition sizes (1 or 3 banks), bank selection uses modulo folding, which causes slightly uneven bank utilisation.
Partitioning is controlled via the l1d_private memory-mapped register in the cluster peripheral. The interconnect (tcdm_cache_interco) uses a runtime-configurable address rotation scheme to present a dense index space to each cache bank regardless of partition mode, preserving full SRAM utilization. The refill unit applies the inverse rotation before issuing misses to the NoC.
The boundary between private and shared address regions is configurable at runtime via l1d_addr(...):
- Addresses ≥ boundary are classified as private
- Addresses < boundary are classified as shared
The default boundary is 0xA000_0000. This means data in .pdcp_src (at 0xA000_0000+) is private by default, and data in .data (at 0x8000_0000+) is shared by default. The boundary can be raised or lowered at runtime to reclassify data regions without moving them in memory.
Changing either the partition mode or the boundary address while the cache contains valid data requires a flush first.
Flush instructions are issued through the cluster peripheral and dispatched to cache controllers via a two-level controller:
- Cluster level (
cachepool_peripheral): holds the instruction and a per-tile one-hot tile-select mask. Issues the instruction to all tiles simultaneously and tracks completion at tile granularity. - Tile level (
cachepool_tile): receives the instruction, determines which of its local cache controllers to activate based on the partition field, and returns a single ready pulse to the cluster controller when all targeted controllers finish.
insn value |
Operation | Banks targeted | Tile select |
|---|---|---|---|
2'b00 |
Flush private | Private banks only | Per-tile one-hot mask |
2'b01 |
Flush shared | Shared banks only | All tiles (forced) |
2'b10 |
Flush all | All banks | All tiles (forced) |
2'b11 |
Invalidate (init) | All banks | All tiles (forced) |
For insn != 2'b00, the peripheral sets the tile-select mask to all-ones for consistency.
Cluster-wide entry points (software/snRuntime/src/l1cache.c) — each of these must be called by all cores; they fence, barrier, have core 0 issue the register write(s), and barrier again before returning, so callers don't need to gate by core ID themselves:
// Flush all banks in all tiles
l1d_cluster_flush();
// Flush private banks in selected tiles (one-hot tile mask, bits 0-63)
l1d_cluster_private_flush(uint64_t tile_mask);
// Flush shared banks in all tiles
l1d_cluster_shared_flush();
// Set the number of private cache banks per tile (0..NumCache)
l1d_part(uint32_t size);
// Set the private/shared address boundary (addr >= boundary is private)
l1d_addr(uint32_t addr);
// Set the crossbar bank-selection bit offset (clamped to >= cacheline width)
l1d_xbar_config(uint32_t offset);Lower-level, single-shot primitives (l1d_flush(), l1d_private_flush(), l1d_shared_flush(), l1d_commit(), l1d_wait()) exist for building custom sequences but issue only the register write — the caller is responsible for cluster-wide fence/barrier synchronization around them.
l1d_addr() has no hardware completion status bit (unlike flush, which polls L1D_FLUSH_STATUS via l1d_wait()); its peripheral register write is posted over the interconnect, so it bridges the gap with a fixed-cycle delay on core 0 before the final barrier.
Changing the partition mode, boundary address, or crossbar offset while the cache contains valid data requires a flush first —
l1d_xbar_config()andl1d_part()flush internally;l1d_addr()does not (call a cluster flush before it if needed).
Each tile tracks completion per cache controller using a cache_flush_q register (one bit per controller). The tile asserts a one-cycle ready pulse to the cluster controller when all targeted controllers have finished. The cluster controller uses a per-tile lock register to track in-flight flushes; a new instruction cannot be issued until all selected tiles report completion.
Cache accesses from cores and remote tiles are gated (l1d_busy) while a flush is in progress, preventing stale hits during the flush window.
A simulation-only monitor (hardware/tb/cachepool_monitor.sv), instantiated alongside the DUT in the testbench, tracks per-session traffic and performance counters — a session is the interval between toggles of the SPATZ_STATUS.SPATZ_CLUSTER_PROBE peripheral bit. Software brackets a region of interest with:
start_kernel(); // opens a new monitoring session
// ... code to profile ...
stop_kernel(); // closes the session and dumps counters to file(call from a single core, e.g. if (cid == 0) { ... }; both are declared in software/tests/include/benchmark.h).
Each session dumps counter files under sim/bin/logs/, split into three subfolders:
| Subfolder | Contents |
|---|---|
core/ |
Per-core statistics (monitor_core_g<g>_<t>_c<c>.txt) |
noc/ |
Inter-group L1 NoC, L2 refill mesh, and DRAM channel traffic |
others/ |
Per-tile local/group/remote request locality and congestion |
CI test jobs recreate these subfolders before running (build artifacts exclude sim/bin/logs/) and archive the whole sim/bin/logs/ tree as job artifacts.
For a spatial, time-scrubbable view of NoC traffic (as opposed to cachepool_monitor.sv's per-session text summaries above), a second simulation-only tap, hardware/tb/cachepool_noc_profiling.sv, records cycle-accurate per-router/tile/core state and packet traces to noc_profiling/*.log. util/scripts/noc_profiling_to_vis4mesh.py converts those logs into a dataset for Vis4Mesh (a fork of ueqri/vis4mesh with CachePool-specific fixes), a browser-based NoC traffic visualizer.
-
Enable the profiler and run a kernel (default
noc_profiling ?= 1, so this is on unless you passednoc_profiling=0):make vsim config=<multi-group config> noc_profiling=1 ./sim/bin/cachepool_cluster.vsim software/build/CachePoolTests/test-cachepool-<test>
L1 (inter-group cache-access mesh) logs (
router_g*.log,tile_g*.log) only populate withnum_rg_ports_per_core > 0(a multi-group config); L2 (DRAM-refill mesh) logs (l2_router_g*.log) and per-core PE logs (pe_g*.log) are always populated. -
Convert to a Vis4Mesh dataset:
python3 util/scripts/noc_profiling_to_vis4mesh.py --level l1 \ --num-groups-x <NumGroupsX> --num-groups-y <NumGroupsY> \ --num-tiles-per-group <NumTilesPerGroup> --num-noc-ports-per-tile <NumNoCPortsPerTile> \ --output-dir util/vis4mesh/visdata/<name>
--levelselects the network and channel breakdown:Level Network Channel breakdown l1(default)Inter-group cache-access mesh Per physical link (tile × NoC port) l2DRAM-refill mesh Single (per your read/write/resp split via message type) --num-tiles-per-group/--num-noc-ports-per-tileare only needed forl1. See the script's module docstring for the full data-shape/limitation notes (e.g. hop-distance and transfer-type filters aren't populated yet). -
Build and serve Vis4Mesh, then upload the dataset directory:
make vis4mesh-serve # clones/builds util/vis4mesh (pinned, see util/vis4mesh.version), serves at http://localhost:8000Open the URL, click the upload button, and select the
--output-dirdirectory from step 2.
The default system uses a 32-bit Snitch core with a Spatz RVV accelerator. Double-precision is disabled by default for scalability; enable the FPU flavor (cachepool_fpu.mk) for single/half precision support.
Each core complex has a local stack SPM. Its depth is configured via parameters in config/config.mk (forwarded to RTL). If the stack exceeds the local SPM, it spills into the cache space (indexed with core ID bits).
Cluster peripherals (including the BootROM and memory-mapped registers) are instantiated at the cluster level, outside of the Spatz cluster. The peripheral register block is defined in hardware/cachepool_peripheral/ and generated from an HJSON register description.
Actual values come from
config/config.mk. Example below reflects the defaults used by the generated HJSON/template.
| Start Address | Size | Region | Notes |
|---|---|---|---|
0x0000_0000 |
0x0000_1000 |
Unused | — |
0x0000_1000 |
0x0000_1000 |
Boot ROM | Boot address typically 0x0000_1000 |
0x8000_0000 |
0x2000_0000 |
DRAM | 512 MiB (default in template) |
0xA000_0000 |
— | Private boundary | Default l1d_addr value; addresses ≥ this are private |
0xBFFF_F800 |
0x0000_0200 |
Stack (local) | Example stack window |
0xC000_0000 |
0x2000_0000 |
Uncached | MMIO/peripherals region |
0xC001_0000 |
0x0000_1000 |
UART | UART base inside peripheral window |
SpyGlass lint (optional):
make lint config=cachepool_fpu_4g- To see the exact macros passed to vlog, check
VLOG_DEFSin the Makefile andsim/work/compile.vsim.tcl. - If you change cacheline width,
AXI_USER_WIDTHis derived (supported widths: 128→19, 256→18, 512→17). Unsupported widths error out at generation time. make generateregenerates the FlooNoC package automatically; only runmake update-floonocstandalone if you're iterating on aconfig/floonoc_*.ymltopology file without a full generate.make swandmake vsimare decoupled (hw/sw build independently); rebuild whichever side you changed.- Use
make cleanwhen switching configs to prevent stale build artifacts. - Runtime functions
snrt_tile_id()andsnrt_num_tiles()are available to query tile topology from software. - Changing the partition mode or boundary address while the cache holds valid data requires a flush (
l1d_cluster_flush()or the appropriate cluster-wide partition flush) before reconfiguring. - Set
DEBUG=0to disable+accand speed up simulation (used by CI); default isDEBUG=1for waveform visibility.
CachePool is released under permissive open source licenses. Most of CachePool's source code is released under the Apache License 2.0 (Apache-2.0), see LICENSE. The code in hardware/ is released under Solderpad v0.51 (SHL-0.51), see hardware/LICENSE.
