SIMTiX is an open-source, GPU-inspired SIMT (Single-Instruction, Multiple-Threads) accelerator written from scratch in SystemVerilog and attached to a 5-stage pipelined RISC-V host CPU. The host offloads data-parallel kernels — vector add, matrix multiply, divergent control kernels — to the accelerator over a memory-mapped command interface, exactly the way a discrete GPU is driven by its host.
The goal of the project is to build, and to make legible, every moving part of a modern throughput machine: warps, a warp scheduler that hides memory latency, eight SIMT lanes over a banked vector register file, true control divergence with a per-warp reconvergence stack, an address- coalescing memory engine, and an on-chip shared-memory scratchpad — then to take the whole thing through FPGA synthesis on a Zynq UltraScale+ and report real area / timing / power, plus a small micro-architectural energy study.
Status: complete. Milestones M0 → M13 are all implemented, verified, and pushed; CI (lint + simulation regression) is green on every push. The design now closes as a complete chip — host CPU + accelerator + on-chip shared memory in one synthesizable top — and is taken all the way through place & route to a timing-closed bitstream (
chip_top.bit, meets 100 MHz post-route). See the roadmap.
- Highlights
- What makes it SIMT (and not just a vector unit)
- System architecture
- Microarchitecture: the
warp_poolengine - How it works, end to end
- Programming model & kernel ISA
- Build & test
- Results
- Engineering highlights
- Repository layout
- Roadmap / milestones
- Limitations & future work
- Prior art
- License
- True SIMT, not SIMD. Per-thread control flow is handled in hardware with a
per-lane active mask and a per-warp reconvergence stack — data-dependent
if/loops work without a compiler. - Latency hiding by warp interleaving. A round-robin scheduler issues one warp per cycle while a background memory engine services another warp's loads/ stores — the classic GPU trick, measured working in simulation.
- Memory coalescing. Eight per-lane accesses that fall on one 32-byte line are merged into a single line transaction; a contiguous warp access costs 1 transfer instead of 8 (8× fewer in the regression).
- On-chip shared memory. A per-warp scratchpad aperture serves a warp's lanes without touching the global port — staging reused data there cuts global traffic.
- Taken to silicon-cost. Out-of-context synthesis on the ZCU104 (xczu7ev) with full area / timing / power, and optimization passes that bank both the register file (M8) and the scratchpad (M9) into distributed RAM: from 170.9k LUT / 44.1k FF down to 30.1k LUT (13%) / 4.6k FF (1%), timing met at 100 MHz (also meets 125 MHz), 0.81 W.
- A real research angle. A divergence-aware lane clock-gating energy study quantifies 0% → 22.5% lane-datapath energy saved as divergence rises.
- Reproducible & open. 100% open-source flow (Verilator); kernels assemble with
stock
riscv32GCC and their machine code is embedded in the testbenches so CI stays toolchain-free.
Default configuration (rtl/accel/simtix_pkg.sv):
| Parameter | Value | Meaning |
|---|---|---|
NUM_LANES |
8 | physical SIMT lanes (threads executed in lockstep / cycle) |
WARP_SIZE |
8 | threads per warp |
NUM_WARPS |
4 | hardware-resident warp slots |
XLEN |
32 | data width (RV32) |
| VRF | 4 × 8 × 32 × 32b | warps × lanes × regs × bits = 32,768 bits of register state |
SDEPTH |
8 | reconvergence-stack depth (branch nesting limit) |
LINE_WORDS |
8 | words per coalesced cache line (a 256-bit / 32-byte line) |
SCRATCH_WORDS |
64 | 32-bit words of scratchpad per warp |
MMIO_BASE |
0x8000_0000 |
accelerator command-register aperture |
SCRATCH_BASE |
0x4000_0000 |
scratchpad address aperture |
A SIMD/vector unit applies one operation to a vector of data; it has no notion of per-element control flow. SIMT keeps the illusion that each thread (lane) has its own program counter. When a branch sends lanes in different directions, the hardware must execute both sides with the correct subset of lanes active and then reconverge them. SIMTiX implements that machinery explicitly:
if (tid & 1) // odd lanes take the branch, even lanes don't -> DIVERGENCE
x += 1000; // executed with only the odd lanes active
// ... both groups rejoin here -> RECONVERGENCEThis is the line that separates SIMTiX from a RISC-V Vector (RVV) core, and it is the heart of milestone M5.
The host is a conventional 5-stage pipelined RISC-V core (rtl/cpu/, reused and
unmodified). The accelerator hangs off the CPU's data bus; a one-line address
decode in rtl/soc/soc_top.sv routes accesses in the MMIO aperture to the
accelerator and everything else to the CPU's local RAM. A normal RISC-V program
therefore launches a kernel purely with sw/lw instructions — no custom opcodes.
┌──────────────────────────┐
│ 5-stage RISC-V host CPU │ driver program:
│ IF ID EX MEM WB │ sw kernel_pc/base_a/b/c/n -> MMIO
└─────────────┬────────────┘ sw 1 -> CTRL.GO ; poll STATUS.DONE
│ data bus (ALUResultM / WriteDataM / ReadDataM)
┌─────────▼─────────┐
│ address decode │ addr >= 0x8000_0000 ? accelerator : data RAM
└────┬─────────┬────┘
(< MMIO) │ │ (>= MMIO)
┌────────▼──┐ ┌───▼─────────────────────────────────────────────────┐
│ data RAM │ │ simt_accel (accelerator top) │
└───────────┘ │ ┌────────────┐ ┌───────────────────────────────┐ │
│ │ mmio_regs │──▶│ dispatcher FSM (GO→run→done) │ │
│ └────────────┘ └──────────────┬────────────────┘ │
│ ┌──────────────▼────────────────┐ │
│ │ warp_pool │ │
│ │ the entire SIMT engine │ │
│ └──────────────┬────────────────┘ │
└──────────────────────────────────┼───────────────────┘
imem (kernel code) ◀┴▶ dmem (256-bit line)
shared memory
mmio_regs— the command/status registers the host writes/reads.simt_accel— wraps the registers, a small dispatcher FSM, and the engine. The dispatcher pulsesstartonGO, then counts cycles until the grid retires (exposed as theCYCLESperformance register).warp_pool— the project: scheduler, lanes, register file, divergence stack, memory engine and scratchpad, all in one tightly-integrated module.
| Offset | Name | R/W | Description |
|---|---|---|---|
0x00 |
KERNEL_PC |
W | byte address of the kernel's first instruction |
0x04 |
BASE_A |
W | argument 0 base pointer |
0x08 |
BASE_B |
W | argument 1 base pointer |
0x0C |
BASE_C |
W | argument 2 base pointer |
0x10 |
N |
W | number of threads (grid size) to launch |
0x14 |
CTRL |
W | bit0 = GO (write 1 to launch) |
0x18 |
STATUS |
R | bit0 = DONE, bit1 = BUSY |
0x1C |
CYCLES |
R | cycle count of the last kernel (performance) |
warp_pool holds NUM_WARPS (4) warp slots resident at once. Each slot owns
its own architectural state; all slots share a single fetch/decode/ALU datapath
and a single data port. A round-robin scheduler issues one warp per cycle, and a
background memory engine advances one coalesced transaction per cycle — so a warp's
memory latency is overlapped with other warps' compute.
┌───────────────────────────────────────────────┐
round-robin ─────▶ │ pick one READY warp (rr_ptr) │
scheduler └───────────────────┬───────────────────────────┘
│ cur_pc, cur_mask (top of that warp's stack)
┌───────────────────▼───────────────────────────┐
shared fetch ─────▶ │ fetch @cur_pc → decode (RV32I + mul + csrr) │
& decode └───────────────────┬───────────────────────────┘
│ one decoded instruction, broadcast to all lanes
┌──────────────────────────────────▼─────────────────────────────────┐
│ lane0 lane1 lane2 lane3 lane4 lane5 lane6 lane7 │ ← "single
│ ALU ALU ALU ALU ALU ALU ALU ALU │ instruction,
│ [cur_mask[l] gates writeback + clock-enable per lane] │ multiple
└──────────────────────────────────┬─────────────────────────────────┘ threads"
│ results
┌──────────────────────────────────▼─────────────────────────────────┐
│ Vector Register File — 8 per-lane DISTRIBUTED-RAM banks (LUTRAM) │
│ vrf[lane][{warp,reg}] : 1 write port + 2 async read ports / bank │
└──────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Background memory engine (runs in parallel with the scheduler): │
│ • coalesces a warp's 8 lane addresses by 32-byte line │
│ • 1 line transaction / cycle on the 256-bit data port │
│ • scratchpad accesses bypass the global port (1 cycle / warp) │
└─────────────────────────────────────────────────────────────────────┘
- Reconvergence stack —
stk_npc / stk_rpc / stk_maskwith stack pointersp. The top-of-stack frame defines the warp's live PC and active mask. - Vector register file — 32 RV32 registers per lane, per warp.
- Scratchpad —
SCRATCH_WORDSwords of on-chip shared memory. warp_base— the warp's base thread id (for thetidCSR /a0).
A wstate field tracks each slot: W_EMPTY → W_RUN ⇄ W_MEM → W_DONE. The spawner
drops grid warps 0,1,2,… into free/retired slots and recycles a slot the moment
its warp retires, so a grid with more warps than slots still runs.
Each warp carries a small reconvergence stack. A branch is resolved per lane:
- Uniform (all active lanes agree) → just retarget the current PC; no divergence.
- Divergent (lanes disagree) → push a "continue" frame so the two lane groups
run in turn, then pop and reconverge at the join PC (
RPC) recorded in the frame. Nesting works up toSDEPTH.
The supported, compiler-free convention is single-sided if (cond) { … } and
divergent loops (one side falls through to the join). dbg_divergences counts
divergent-branch events for verification.
The data port is a whole 256-bit line (8 words). Each cycle the memory engine
picks the line of the lowest still-pending lane and gathers every pending lane
that shares that line into one transaction. A contiguous warp access (A[tid])
collapses to 1 line transfer; a fully scattered access costs up to 8.
dbg_mem_txns counts global line transactions.
Accesses whose effective address lands in the SCRATCH_BASE aperture are served
from a per-warp on-chip scratchpad and never reach the global port. Staging reused
data (e.g. a broadcast matrix row) there cuts global traffic; dbg_scratch_txns
counts scratchpad transactions. The scratchpad is shared across a warp's lanes
(lane 0's store is visible to lane 3's load), so M9 implements it as a single
distributed-RAM (LUTRAM) bank with the memory engine serializing one lane per
cycle — the same coalescing-style drain the global port uses — which removes 8.2k
flip-flops while keeping shared-memory semantics exact.
The VRF is 32,768 bits of state read as vrf[issue_w][lane][rs] with both the
warp and register index variable — which, if built from flip-flops, synthesizes
into a 128:1 multiplexer per lane per read port (the dominant area and critical
path on FPGA). M8 banks it into 8 independent per-lane distributed-RAM (LUTRAM)
banks — one synchronous write port and two async read ports each — so the
addressing is done by the RAM primitive instead of LUT muxes. Two supporting
mechanisms keep behaviour bit-identical:
- Valid bits replace the one-cycle 32-register spawn clear (a 32-write-port
pattern a RAM can't express): an unwritten register reads its spawn seed
(
a0=tid,a1..a4=args, else 0). - A single arbitrated write port per lane: the issue stage's compute writeback and the memory engine's load writeback can collide, so the memory write wins and the compute writeback is squashed and re-issued next cycle (idempotent, so results are unchanged).
The win: LUTs 170.9k → 77.0k, FFs 44.1k → 13.3k, and the design now meets timing — see Results and docs/m8_lutram.md. M9 then moves the scratchpad to LUTRAM the same way and re-enables timing-driven synthesis, landing at 30.1k LUT / 4.6k FF — see docs/m9_scratchpad.md.
A complete launch of a vector-add grid (C[tid] = A[tid] + B[tid]):
- Command. The host writes
KERNEL_PC, BASE_A/B/C, Nto the MMIO page, then writesCTRL.GO = 1. - Dispatch.
simt_accel's FSM pulsesstart;warp_poollatches the grid and computestotal_warps = ceil(N / WARP_SIZE). - Spawn. Free slots are filled with warps. Each lane is seeded by convention
(
a0 = tid,a1..a4 = args) — for free, via the M8 valid-bit seed mux. - Issue. Every cycle the round-robin scheduler picks one
W_RUNwarp, fetches at its top-of-stack PC, decodes once, and executes across all 8 lanes; onlycur_masklanes write back. - Memory. A
lw/swhands the warp's active-lane addresses to the background engine (warp →W_MEM) and keeps issuing other warps. The engine coalesces and drains the access over one-or-more line transactions, writes results back to the VRF, and returns the warp toW_RUN. - Divergence. A branch where lanes disagree pushes a stack frame; the groups run in turn and pop/reconverge at the join PC.
- Retire.
ecallretires a warp (slot →W_DONE, recyclable). When nothing is left to spawn or run, the pool pulsesdone; the dispatcher latchesCYCLESand setsSTATUS.DONE. The host's poll loop falls through and reads results from memory.
Kernels are written in the RV32I subset (plus mul and a tid CSR) that the
lanes implement, so they assemble with the standard
riscv32-unknown-elf-gcc -march=rv32im. At launch each thread is seeded by register
convention:
| Register | Contents at kernel entry |
|---|---|
a0 (x10) |
thread id (tid) |
a1 (x11) |
base pointer A |
a2 (x12) |
base pointer B |
a3 (x13) |
base pointer C |
a4 (x14) |
thread count N |
Thread identity is also available from CSR TID (0xCC0). A thread terminates with
ecall.
Supported instruction classes: ALU reg/imm (add sub and or xor sll srl sra slt sltu + immediates), lui/auipc, mul (RV32M, low 32 bits), loads/stores
(lb lh lw lbu lhu, sb sh sw), branches (beq bne blt bge bltu bgeu), jumps
(jal jalr), csrr rd, tid, and ecall. Full details in
docs/isa.md.
Example — vector add (kernels/testbench):
vadd_kernel:
slli t0, a0, 2 # t0 = tid * 4 (byte offset)
add t1, a1, t0 # &A[tid]
add t2, a2, t0 # &B[tid]
add t3, a3, t0 # &C[tid]
lw t4, 0(t1) # A[tid]
lw t5, 0(t2) # B[tid]
add t6, t4, t5
sw t6, 0(t3) # C[tid]
ecall # thread retiresHost side (pseudocode):
ACCEL->kernel_pc = vadd_kernel;
ACCEL->base_a = A; ACCEL->base_b = B; ACCEL->base_c = C;
ACCEL->n = 1024;
ACCEL->ctrl = GO;
while (!(ACCEL->status & DONE)) {}The repo ships divergence (kernels/divergence/) and matmul (kernels/matmul/,
naïve vs. scratchpad-staged) kernels used by the regression.
Everything runs under Verilator (5.030) — the exact flow CI uses, so a green badge means the regression passed. No vendor license required.
make lint # strict lint of the accelerator + SoC + reference cores (6 targets)
make test # build & run the self-checking SystemVerilog regression (5 testbenches)The regression covers the single-thread reference lane, the single-warp reference, the full multi-warp pool (coalescing, divergence, scratchpad, the energy study), the accelerator with its MMIO handshake, and the complete SoC where the host CPU launches a kernel over MMIO end to end. Kernel machine code is embedded in the testbenches, so CI needs no RISC-V toolchain.
FPGA PPA (optional, needs Vivado):
cd fpga
vivado -mode batch -source synth_ooc.tcl # OOC synth of simt_accel on xczu7ev
# -> fpga/reports/post_synth_{util,timing,power}.rpt| Testbench | What it proves | Result |
|---|---|---|
tb_lane |
single-thread RV32I core (M1 reference) | ✅ PASS |
tb_warp |
8-lane lockstep warp + banked VRF (M2 reference) | ✅ PASS |
tb_warp_pool |
multi-warp scheduler, coalescing, divergence, scratchpad, energy study | ✅ PASS |
tb_simt_accel |
accelerator + MMIO launch handshake; vadd C[tid]=A+B correct |
✅ PASS |
tb_soc_top |
host CPU launches the accelerator over MMIO, end to end | ✅ PASS |
Key measured behaviours (from tb_warp_pool):
- Coalescing: a contiguous warp access costs 3 line transactions vs 24 for a fully scattered one — 8× fewer.
- Latency hiding: 4 warps finish in 49 cycles vs 60 if serialized (4 × 15) — ~81% of the serial memory cost hidden behind other warps' compute.
- Divergence: a single-sided
if(tid&1)kernel records exactly 2 divergent events (one per warp) and every lane's result is correct after reconvergence. - Scratchpad: staging a reused matrix row on-chip cuts global line transactions from 17 → 11.
The SIMT active mask is also the natural per-lane clock-enable: lanes masked off by divergence do no useful work, so a real chip would clock-gate them. The engine counts committed lane-instructions vs. active lanes and reports the dynamic energy a gated design would save. Across kernels of rising divergence intensity:
| Kernel | Divergence | Lane utilisation | Lane-datapath energy saved |
|---|---|---|---|
| convergent (vadd) | none | 100.0% | 0.0% |
light (if(tid&1)) |
mild | 95.8% | 4.2% |
heavy (3× nested if) |
strong | 77.5% | 22.5% |
The monotonic relationship is asserted in CI. Details: docs/m7_energy.md.
Out-of-context synthesis of simt_accel, evolved over three builds. M7b used a
flip-flop register file and showed it dominating area and timing; M8 banked the VRF
into distributed RAM; M9 did the same for the shared scratchpad and — the netlist
now being small enough — re-enabled timing-driven optimization:
| Metric | M7b (FF VRF) | M8 (LUTRAM VRF) | M9 (+ LUTRAM scratch) |
|---|---|---|---|
| Synthesis flow | -no_timing_driven |
-no_timing_driven |
timing-driven |
| Target clock | 5.0 ns / 200 MHz | 10.0 ns / 100 MHz | 10.0 ns / 100 MHz |
| CLB LUTs | 170,868 (74.2%) | 77,025 (33.4%) | 30,088 (13.1%) |
| └ as distributed RAM | 20 | 1,300 | 1,428 (VRF + scratch) |
| CLB registers (FF) | 44,112 (9.6%) | 13,322 (2.9%) | 4,590 (1.0%) |
| DSP48E2 | 24 | 24 | 24 |
| Block RAM / URAM | 0 / 0 | 0 / 0 | 0 / 0 |
| Setup WNS | −3.213 (violated) | +2.719 (MET) | +2.254 (MET) |
| Critical-path delay | 8.213 ns | 7.281 ns | 7.746 ns |
| Max Fmax | 121.8 MHz | 137.3 MHz | 129.1 MHz |
| On-chip power (vectorless) | 1.673 W @200 MHz | 0.841 W @100 MHz | 0.811 W @100 MHz |
The M9 design meets timing at 100 MHz with +2.25 ns slack and the WNS-vs-period sweep shows it also closes at 125 MHz; the path is an irreducible single-cycle LUTRAM-read → 32-bit ALU (incl. a DSP multiply) → writeback. Read the M8→M9 table honestly: the flip-flop drop (13.3k → 4.6k) is the scratchpad (8.2k FFs removed), while the LUT drop (77k → 30k) is mostly the timing-driven optimization phase finally fitting in the host's RAM once both big banks are LUTRAM. (Power is a vectorless estimate; dynamic power scales with frequency, so cross-clock figures are not directly comparable.) Full analysis: docs/m8_lutram.md, docs/m9_scratchpad.md.
Host note. These runs were done on an 8 GB laptop. The flow is tuned for that (
-flatten_hierarchy none, capped threads); M7b/M8 also needed-no_timing_drivento dodge a 4 GB RAM thrash, a workaround M9's smaller netlist no longer requires. The writeups document the RAM-pressure pitfalls and the working recipe.
M10 synthesizes the complete chip — the RISC-V host pipeline, the accelerator, the on-chip shared memory, and the driver ROM — in one timing-driven OOC run:
| Instance | LUTs | LUTRAM | FFs | DSP |
|---|---|---|---|---|
| chip_top (whole chip) | 34,340 (14.9%) | 3,348 | 6,171 (1.3%) | 24 |
cpu (riscv_pipeline) |
1,451 | 0 | 1,555 | 0 |
u_accel (simt_accel) |
29,892 | 1,428 | 4,583 | 24 |
u_mem (shared_mem) |
2,934 | 1,920 | 0 | 0 |
The chip meets timing at 100 MHz (+0.253 ns), uses 0 BRAM, and draws 0.964 W.
The host CPU is small (1.5k LUT / 1.6k FF), the accelerator dominates (~87 % of LUTs),
and the 4 KB shared memory is entirely LUTRAM (0 flip-flops) thanks to its 8-bank
layout. The critical path is the accelerator's own single-cycle compute path
(warp-state → ALU+DSP multiply → VRF LUTRAM write); the CPU and shared memory are
not on it. Full analysis: docs/m10_chip.md.
M11 takes the same chip_top through the real device flow (in-context
synth → opt → place → phys_opt → route → bitstream) and produces a routed,
timing-closed image, fpga/chip_top.bit:
| Post-route sign-off (xczu7ev) | Value |
|---|---|
| Setup WNS / Hold WHS | +0.000 ns / +0.005 ns — all constraints met @ 100 MHz |
| Failing endpoints | 0 / 37,516 (setup), 0 / 37,516 (hold) |
| Errors / critical warnings | 0 / 0 |
| Area | 34,850 LUT (15.1%) / 6,130 FF / 3,348 LUTRAM / 24 DSP / 0 BRAM |
| Power | 1.086 W (0.492 dynamic + 0.594 static) |
| Bitstream | chip_top.bit, 19.3 MB |
Board files for the ZCU104 aren't installed here, so the 35 top-level I/O are auto-placed and the unconstrained-I/O DRCs downgraded — the deliverable is a real placed-and-routed, timing-closed bitstream of the whole chip, not board peripheral bring-up. Full writeup: docs/m11_impl.md.
A few problems worth calling out, because solving them is most of the work:
- Latched memory-replay context. The shared decode always tracks the
currently-issued warp, but the background engine is replaying a different
warp's access — so the access context (per-lane address, store data,
funct3,rd, mask) is latched at issue. Missing this is a subtle multi-warp bug. - The reconvergence-stack convention. Forward branches (
if) and backward branches (loops) push frames with different join-PC and mask choices so a single mechanism handles both, with the base frame guarding the bottom of the stack. - Making the register file inferable as RAM. A 2-D
vrf[lane][addr]written in aforloop is misread by Vivado as a "3-D RAM" and dissolves to flip-flops; splitting it into one 1-D array per lane in agenerateblock withram_style="distributed"is what actually maps to LUTRAM. The one-cycle spawn clear and the dual-writer both had to be redesigned (valid bits; arbitrated write port with idempotent re-issue) to keep a single write port — without changing a single test result. - CI stays toolchain-free. Kernels are assembled offline with
riscv32GCC and their hex is embedded in the testbenches, so GitHub Actions only needs Verilator.
rtl/accel/ SIMT accelerator RTL (the project)
simtix_pkg.sv parameters, MMIO map, ISA/ALU constants
mmio_regs.sv host command/status registers
warp_pool.sv the SIMT engine: scheduler, lanes, VRF (LUTRAM), divergence
stack, coalescing memory engine, scratchpad
simt_accel.sv accelerator top: MMIO + dispatcher FSM + warp_pool
lane.sv M1 single-thread reference core (kept, still linted/tested)
warp.sv M2 single-warp reference (kept, still linted/tested)
rtl/cpu/ reused 5-stage pipelined RISC-V host core (Verilog)
rtl/soc/ chip_top.sv — complete chip: CPU + accelerator + shared memory
shared_mem.sv — 8-bank LUTRAM on-chip shared memory (M10)
cpu_driver_rom.sv — host driver program (launch + readback)
soc_top.sv — earlier CPU + accelerator + MMIO decode (M1.3)
sim/ Verilator build/lint Makefile (delegated to from the root)
tests/ self-checking SystemVerilog testbenches
kernels/ data-parallel kernels (divergence, matmul naïve vs. scratchpad)
fpga/ synth: synth_ooc.tcl (accel), synth_chip.tcl (whole chip, OOC)
impl_chip.tcl — full place & route → chip_top.bit (M11)
docs/ architecture, ISA, roadmap, energy study, LUTRAM writeup
Each milestone is a self-contained, demoable, green-in-CI step.
| # | Milestone | Proves | Status |
|---|---|---|---|
| M0 | Scaffold + CI + host builds in Verilator | tooling works end to end | ✅ |
| M1 | MMIO launch handshake + single-lane kernel execution | offload + one thread runs a real kernel | ✅ |
| M2 | 8 SIMT lanes + banked vector register file | lockstep data-parallel execution | ✅ |
| M3 | Multiple warps + round-robin scheduler | latency hiding by warp switching | ✅ |
| M4 | LSU + loads/stores + address coalescing | realistic memory behaviour | ✅ |
| M5 | Control divergence: active mask + reconvergence stack | true SIMT | ✅ |
| M6 | Shared-memory scratchpad + matmul reuse benchmark | on-chip reuse story | ✅ |
| M7a | Divergence-aware lane clock-gating energy study | the research contribution (0→22.5%) | ✅ |
| M7b | FPGA / PPA: OOC synth on ZCU104 | real silicon-cost numbers | ✅ |
| M8 | Register file in distributed RAM + timing closure | LUTs −55%, FFs −70%, timing met | ✅ |
| M9 | Scratchpad in distributed RAM + timing-driven synth | 30.1k LUT / 4.6k FF, timing met (meets 125 MHz) | ✅ |
| M10 | Complete chip: CPU + accelerator + on-chip shared memory | self-contained SoC, result=964 in HW; 34.3k LUT / 6.2k FF, meets 100 MHz | ✅ |
| M11 | Full implementation + bitstream (place & route → chip_top.bit) |
post-route timing-closed @ 100 MHz (WNS +0.000, 0 failing of 37.5k), 19 MB bitstream | ✅ |
| M12 | CPU-side input loading (host writes A/B into shared memory at runtime) | full heterogeneous-SoC flow; result=964 proven CPU-driven (no preloaded inputs) | ✅ |
| M13 | Post-implementation functional sim (routed gate-level netlist in xsim) | UNISIM gate netlist reproduces result=964 — synth + P&R sign-off on the real hardware | ✅ |
Full detail in docs/roadmap.md.
- Scope of divergence. General two-sided
if/elsewhere both arms are non-empty before the join is out of scope; write it as two single-sidedifs. The convention is compiler-free by design. - One memory access in flight. A single data port serves the whole engine; a warp wanting memory while the port is busy waits its turn (no starvation — the scheduler keeps advancing).
- Small, fixed grid geometry.
WARP_SIZE == NUM_LANES == 8,NUM_WARPS == 4by default; all are parameters. - Next optimizations. The big register banks (VRF M8, scratchpad M9) are now in LUTRAM; remaining ideas: move the SIMT reconvergence stacks (now the dominant FF user) into RAM, tighten the FPGA clock to the 125 MHz the design already meets, and cross-check the modelled clock-gating energy against a switching-activity power run.
SIMTiX is a compact, legible take on ideas explored by larger open projects, useful to position against: Vortex (Georgia Tech full RISC-V GPGPU), Ventus (RVV-based GPGPU), Nyuzi / Cyclone (open SIMT GPGPU with an excellent divergence writeup), Simty (minimal SIMT RISC-V, closest in spirit), and the GPGPU-Sim / MIAOW ecosystem. SIMTiX's niche is being small enough to read end to end while still implementing real divergence, coalescing, shared memory, and a measured FPGA PPA + energy result.
Apache-2.0 — see LICENSE.