TACCEL is an experimental INT8 transformer accelerator and compiler stack for running a quantized DeiT-tiny vision transformer workload in RTL. The project contains both the hardware model and the software toolchain needed to assemble, compile, simulate, and compare accelerator programs against a Python golden model.
The current target workload is facebook/deit-tiny-patch16-224.
This repo is organized around three cooperating layers:
software/: Python ISA, assembler, compiler, quantizer, golden model, and RTL-vs-golden comparison tools.rtl/: SystemVerilog RTL for the accelerator, plus Verilator and cocotb testbenches.docs/: design plans, debug plans, synthesis notes, and historical investigation notes.
The most important design principle is that the RTL is verified against the same compiler and golden model used by the software stack. The goal is not just to unit-test isolated modules, but to run compiler-generated programs through RTL and compare their architectural outputs against the golden model.
The baseline compiler flow targets DeiT-tiny:
| Property | Value |
|---|---|
| Sequence length | 197 tokens, padded to 208 |
| Embedding dimension | 192 |
| Attention heads | 3 |
| Head dimension | 64 |
| MLP hidden dimension | 768 |
| Transformer blocks | 12 |
| Classifier outputs | 1000 |
The current host/runtime contract assumes:
- Patch embedding is performed on the CPU.
- The accelerator starts from pre-embedded INT8 patch tokens in DRAM.
- DRAM contains weights, biases, FP16 scale tables, CLS token, and position embeddings.
- Accelerator SRAM offsets are expressed in 16-byte units.
The RTL implements a small fixed-function transformer accelerator:
| Unit | Purpose |
|---|---|
| Fetch/decode/control | Fetch 64-bit instructions from DRAM and dispatch work |
| DMA engine | Move data between DRAM and SRAM buffers |
| Systolic array | 16x16 INT8 x INT8 matrix multiply with INT32 accumulation |
| Blocking helper engine | Local copies, requantization, scale multiply, VADD, DEQUANT_ADD |
| SFU | LayerNorm, Softmax, GELU, attention@V helper paths |
| SRAM subsystem | ABUF, WBUF, and ACCUM dual-port SRAM models |
The architectural SRAM buffers are:
| Buffer | Size | Data view | Addressing unit |
|---|---|---|---|
| ABUF | 128 KB | INT8 activations | 16 bytes |
| WBUF | 256 KB | INT8 weights / FP16 params / INT32 bias | 16 bytes |
| ACCUM | 64 KB | INT32 accumulators | 16 bytes |
The custom ISA is defined in software/taccel/isa and implemented in RTL by
the fetch, decode, control, DMA, helper, SFU, and systolic units.
Implemented instruction groups:
- System/setup:
NOP,HALT,SYNC,CONFIG_TILE,SET_SCALE,SET_ADDR_LO,SET_ADDR_HI - Data movement:
LOAD,STORE,BUF_COPY - Matrix compute:
MATMUL - Quantization and helper compute:
REQUANT,REQUANT_PC,SCALE_MUL,VADD,DEQUANT_ADD - SFU compute:
SOFTMAX,LAYERNORM,GELU,SOFTMAX_ATTNV
See software/docs/isa_spec.md and docs/rtl_plan.md for the detailed ISA
contract.
The Python toolchain provides:
- ISA definitions and binary encoding/decoding.
- Text assembler and disassembler.
- INT8 quantization and calibration utilities.
- DeiT-tiny graph extraction and tile-level code generation.
- A sequential golden-model simulator.
- Debug and sign-off tools for comparing RTL runs against golden traces.
Useful entry points:
software/tools/asm.py: assemble text assembly toprogram.bin.software/tools/disasm.py: disassemble a binary program.software/tools/compile_model.py: compile a model into an accelerator program.software/tools/run_golden.py: run a program in the Python golden model.software/tools/compare_rtl_golden.py: W8A16 RTL-vs-golden bit-exact parity check on one (program, FP16 patch) pair. Drives the Verilator runner andSimulatorW8A16through the same program, slices the FP16 classifier logits from both ABUF images, and asserts byte-equality on theuint16view.software/tools/batch_compare_rtl_golden.py: load-bearing acceptance gate. Iteratescompare_rtl_goldenover the 20 frozen benchmark images and prints a PASS/FAIL summary with the first FP16 logit that diverges per failing image.
The software toolchain supports two precision modes, selectable via
--mode on the user-facing tools (compile_model.py, run_golden.py,
profile_memory.py):
| Mode | Weights | Activations | Accumulators |
|---|---|---|---|
w8a16 (default, shipping) |
INT8 per-channel → FP16 dequant in DRAM | FP16 | FP32 (mixed-precision standard) |
w8a32 |
INT8 per-channel → FP32 dequant in DRAM | FP32 | FP32 (reinterpret ACCUM) |
W8A16 is the shipping path: per-channel INT8 weights are dequantized into FP16 in DRAM at compile time, FP16 activations live in ABUF, and the FP32 accumulator holds the matmul partial sums. W8A32 doubles the dequant-weight DRAM footprint in exchange for an extra decimal digit of numerical headroom; it serves as the FP32 weight-quant ceiling reference.
The legacy W8A8 (INT8 weights + INT8 activations) path was removed: the
INT8-activation floor on ViT was the dominant accuracy-loss term and the
calibration plumbing it required (SmoothQuant / Hessian-guided / twin
uniform / per-tensor activation scales) was incompatible with a clean
toolchain. The RTL (rtl/) remains as historical reference — it
implemented W8A8 — and is no longer targeted by the software toolchain.
For the end-to-end accuracy benchmarks:
./.venv/bin/python3 software/tools/benchmark_w8a16.py --max-images 20
./.venv/bin/python3 software/tools/benchmark_w8a32.py --max-images 20benchmark_w8a16.py parallelises the per-image work across processes
by default (--workers=$(nproc)); pass --workers 1 to run sequentially
for debugging.
W8A16 picks up most of the W8A32 accuracy at half the dequant-DRAM
footprint — cos vs FP32 ≥ 0.997, cos vs fake_quant ≥ 0.998. See
docs/precision_modes.md for the motivation behind W8A16 and the load-
bearing accuracy gates.
Codegen ships two thin passes that the compiler runs unconditionally; they cut the DeiT-tiny W8A16 instruction count by ~49% without touching the RTL or weakening the bit-exact gate:
compiler/dma_emitter.py::AddrPlannercaches the value of each address register and uses the M-typedram_offfield (16-bit, ×16-byte) to walk inside a 1 MB window without re-emitting SET_ADDR. Drives SET_ADDR_HI to ≈0 and SET_ADDR_LO down by ~99.7% on DeiT-tiny.compiler/sync_coalesce.py::coalesce_dma_syncsdrops SYNC(0b001) bits the RTL already enforces at issue: helper / SFU ops (BUF_COPY,SCALE_MUL,VADD,SOFTMAX,LAYERNORM,GELU) auto-stall ondma_busyincontrol_unit.sv. The SYNC beforeOP_MATMULand between adjacent DMA ops is load-bearing — the DMA engine has no command queue (dma_engine.svline 187), so a second dispatch pulse during an in-flight LOAD is silently dropped. The pass keeps those SYNCs verbatim.
Net DeiT-tiny W8A16: 1,288,764 → 657,846 instructions (10.3 MB →
5.3 MB of program bytes). The golden simulator's decode() hot path
(~34% of golden runtime) gets a proportional speedup, so the cosine-
gate benchmarks complete faster as well.
Important RTL files:
rtl/src/taccel_top.sv: top-level accelerator integration.rtl/src/control_unit.sv: instruction retirement, dispatch, barriers, and fault handling.rtl/src/fetch_unit.sv: instruction fetch path.rtl/src/decode_unit.sv: instruction decoder.rtl/src/dma_engine.sv: DRAM/SRAM load and store engine.rtl/src/blocking_helper_engine.sv: blocking local helper operations.rtl/src/sfu_engine.sv: SFU operations.rtl/src/systolic/: systolic PE, array, and controller.rtl/src/memory/: SRAM and register-file models.
Verilator is the primary sign-off simulator. cocotb tests exist for additional ISA-visible coverage, but the current debug and sign-off flow is centered on native Verilator benches and the program-level runner.
The project can build the RTL testbench suite and run compiler-generated programs through the Verilator runner. The debug infrastructure can emit snapshots, SRAM write logs, systolic traces, and replay payloads to isolate first divergences against the golden model.
Current numerical and synthesis status:
- All FP32 SFU helpers are now synthesizable SystemVerilog. The full set —
round,add,sub,mul,div,sqrt,exp,erf,gelu,quantize_i8, andfrom_fp16— lives inrtl/src/include/fp32_prim_pkg.svas bit-exactfp32_*_bitsfunctions. The normative spec isrtl/src/include/ARITH_CONTRACT.md. - The grep gate at
rtl/verilator/Makefile'ssynth_gatetarget enforces thatrtl/src/(excludingrtl/src/tb/) contains noDPI-Cimports, norealtypes, and none of$realtobits/$bitstoreal/$rtoi/$itor. - RTL is bit-exact-equivalent to the W8A16 golden model by construction:
the SFU engine (
rtl/src/sfu_engine.sv) and systolic PE (rtl/src/systolic/systolic_pe.sv) call the samefp32_*_bitsprimitives fromrtl/src/include/fp32_prim_pkg.svthatsoftware/taccel/utils/fp32_prim_ref.pymirrors with sequential FP32 left-folds, and the W8A16 golden matmul (software/taccel/golden_model/systolic_w8a16.py) uses an explicit per-PE sequential K-loop to match the array's accumulation order. The sign-off invariant isnp.array_equal(rtl_logits.view(np.uint16), golden_logits.view(np.uint16))— zero ULPs across all 1000 FP16 classifier logits, enforced bysoftware/tools/batch_compare_rtl_golden.pyover the 20 frozen benchmark images. The bit-exact gate is load-bearing: a divergence must be root-caused at the responsible rounding step, not papered over with a tolerance.
Remaining work for FPGA bring-up (documented as future work, not in main):
- SFU FSM serialization — the
F_ROW_COMPUTE,F_ATTN_PREP, andF_ATTN_V_LATCHstates inrtl/src/sfu_engine.svstill execute 208-element computations combinationally. They must be broken into element-serial sub-states sharing one FP datapath instance before timing can close on FPGA. - Vivado synthesis dry-run and target-clock closure (≈300–400 MHz UltraScale+ realistic for FP32 div/sqrt). Portable RTL only — no vendor FP IP, since vendor rounding would break the exact-logit contract.
Python dependencies are listed in software/requirements.txt. A typical local
setup is:
python3 -m venv .venv
./.venv/bin/pip install -r software/requirements.txtRTL simulation requires Verilator and a C++17 compiler. On macOS with Homebrew, for example:
brew install verilatorThe repository currently expects model weights and sample images to live under
software/, for example:
software/pytorch_model.binsoftware/images/frozen_benchmark/000000002006.jpg
Run the Python test suite:
./.venv/bin/python3 -m pytest software/tests -qBuild and run the full native Verilator suite:
make -C rtl/verilator allThe Verilator builds fan out across cores via -j $(nproc) plus
--output-split 20000 in VFLAGS; on a 16-core box a clean
run_program build drops from ~9 min to ~2m30s. Override with
make NPROC=4 ... on shared machines.
Run selected RTL tests:
make -C rtl/verilator test_decode
make -C rtl/verilator test_control
make -C rtl/verilator test_dma
make -C rtl/verilator test_helpers
make -C rtl/verilator test_fp32_prims
make -C rtl/verilator test_sfu
make -C rtl/verilator test_systolic
make -C rtl/verilator test_systolic_qktBuild the full-program RTL runner:
make -C rtl/verilator run_programRun the full 20-image RTL-vs-golden bit-exact parity gate (the load-bearing W8A16 acceptance contract; ~1–2 hours wall time because each image is a full Verilator simulation):
./.venv/bin/python3 software/tools/batch_compare_rtl_golden.py \
--max-images 20Run a single (program, FP16 patches) bit-exact compare directly (useful for first-divergence investigation on one input):
./.venv/bin/python3 software/tools/compare_rtl_golden.py \
--program /tmp/deit_tiny_w8a16.bin \
--patches /tmp/patches_fp16.npy \
--max-cycles 5000000Enable replay-backed QK/SFU regressions when a replay payload bundle exists:
RTL_QKT_REPLAY_DIR=/tmp/rtl_debug_stepaa2/replay_payloads \
make -C rtl/verilator test_sfu test_systolic_qktA major part of this project is the RTL/golden debug workflow. When a full compare fails, the tools can emit:
rtl_summary.json: execution status, cycles, logits, faults, violations.rtl_snapshot_manifest.jsonandrtl_snapshot_data.bin: checkpoint tensor snapshots.first_divergence.json: first raw checkpoint mismatch.effective_first_divergence.json: first mismatch after approved rebasing.- SRAM write logs for ABUF, WBUF, and ACCUM.
- Systolic window traces and hidden snapshots.
- Replay payload bundles for small native Verilator reducers.
The intended workflow is:
- Run full RTL-vs-golden compare.
- Inspect the first raw divergence.
- Use emitted replay payloads to build or run a focused reducer.
- Diff SRAM provenance and checkpoint artifacts.
- Classify the mismatch as a real RTL bug, a replay/source issue, a capture issue, or an approved nonblocking/rounding artifact.
This workflow is documented in more detail in:
docs/rtl_debugging_plan.mddocs/rtl_debug_plan.mdrtl/TESTBENCHES.md
- Prefer adding focused Verilator tests for RTL bugs before broad program-level tests.
- Keep golden-model semantics and RTL-visible semantics aligned; do not rely on Python file boundaries to infer hardware dispatch.
- Be careful with FP32 behavior. NumPy, C++ libm, SV
real, and custom RTL arithmetic can differ by one LSB unless the operation order and rounding points are intentionally frozen. - Do not run multiple Verilator builds that target the same build directory in parallel; they can trample generated archives.
This repository is a useful foundation for an LLM accelerator, especially the ISA/toolchain/test infrastructure and systolic/DMA/control blocks. However, an LLM accelerator would require substantial new architecture:
- autoregressive decode scheduling
- KV-cache layout and update paths
- RoPE/RMSNorm/SwiGLU operator support
- prefill vs. decode tiling strategies
- long-context memory planning
- new golden-model and checkpoint coverage
Treat this codebase as a strong transformer-accelerator prototype rather than a drop-in finished LLM accelerator.
software/CODEBASE.md: detailed software stack walkthrough.rtl/TESTBENCHES.md: RTL testbench ownership and commands.docs/rtl_plan.md: hardware/software contract and RTL roadmap.docs/archive/rtl_synthesis_plan_phase0-4_COMPLETED.md: synthesis-oriented planning notes from the Phase 0–4 DPI/realmigration (now complete).software/docs/isa_spec.md: ISA details.