TinyTPU-isa vs Gemmini: A Matched Comparison¶
A cycle comparison between TinyTPU-isa and a Gemmini elaborated to match it:
int8/int32, the same array size, the same shapes, the same operand
distribution, with both sides measured on RTL. It produces a per-shape cycle
count for each machine over a named measurement window, together with the
patches, pins and commands needed to rebuild either column. Our side needs the
allo conda environment and Vitis HLS cosim; Gemmini’s needs a Chipyard
checkout and Verilator. The design under test is TinyTPU-isa: A Programmable GEMM Accelerator; the
measurements, and every claim this page has withdrawn, are on
TinyTPU-isa vs Gemmini: Results and History.
Quick start¶
Our column, from a clean checkout, in the allo environment with
LLVM_BUILD_DIR exported:
examples/tinytpu/reproduce.sh # checks the five cycle counts
What it checks is The spec, and holding both consumers to it. The parity configurations, which are the ones measured against matched Gemmini across every shape of the matched set:
cd examples/tinytpu
TPU_PARITY_CONFIG=parity-t4 python parity_sweep.py # 10 shapes
TPU_PARITY_CONFIG=parity-t8 python parity_sweep.py # 8 shapes
Gemmini’s column, from the committed patches. ~/chipyard/env.sh activates
Chipyard’s own conda environment and so replaces the allo one; source it in
a separate shell (dev/toolchains.rst).
G=<repo>/examples/tinytpu/gemmini
cd ~/chipyard && git apply $G/chipyard_RoCCAcceleratorConfigs.patch
cd generators/gemmini && git apply $G/gemmini_CustomConfigs.patch
cd software/gemmini-rocc-tests
git apply $G/roccTests_gemmini_h.patch
git apply $G/roccTests_gemmini_params_h.patch
git apply $G/roccTests_Makefile.patch
cp $G/allo_cmp.c $G/allo_bare5.c bareMetalC/
cd ~/chipyard && source env.sh
cd sims/verilator && make CONFIG=Int8Dim4GemminiRocketConfig -j16
./simulator-chipyard.harness-Int8Dim4GemminiRocketConfig +permissive +permissive-off \
../../generators/gemmini/software/gemmini-rocc-tests/build/bareMetalC/allo_cmp-baremetal
One Verilator run takes about 80 s and covers all five shapes; the
accelerator-only window benchmark, allo_bare5, is the same order (~90 s).
The run opens with a boot banner, and that banner is what proves which
hardware produced the numbers – GEMMINI DIM=4 elem_t_bytes=1 for the
matched DIM=4 build. Raw output from the recorded run is in
dev/records/tinytpu/logs/gemmini_int8_dim4.log.
allo_bare5 needs its own make target: the committed
roccTests_Makefile.patch adds only allo_cmp to the tests list. The
full file-by-file account of the Gemmini side is in
Reproducing the Gemmini baseline.
How it works¶
Making the baseline matched¶
Neither stock Gemmini config is a fair reference:
config |
mesh |
dtype |
matched? |
|---|---|---|---|
|
16x16 |
int8/int32 |
no – 16x the array |
|
4x4 |
fp32 |
no – FPU, and fp32 on an FPGA is soft-float |
|
4x4 |
int8/int32 |
yes |
gemmini.GemminiCustomConfigs.int8Dim4Config is defaultConfig – Gemmini’s
own inputType = SInt(8.W), accType = SInt(32.W) – with
meshRows/Columns = 4. Elaboration confirms it: GEMMINI DIM=4
elem_t_bytes=1. allo_cmp.c needed no source change, being written against
elem_t and filled with values in [-4, 4], which is also what
bench_isa.py/cosim.py feed our design.
Both numbers are measured on RTL: ours by Vitis cosim (xsim), Gemmini’s by
rdcycle under Verilator.
One hardware build, workload swept as data¶
Earlier revisions of this page compared per-workload builds against Gemmini’s single elaboration; that is recorded in Earlier measurements and corrections.
The design is now built once – T=4, MAXDIM=16, fixed scratchpad,
vregs and imem – and the shape arrives as data:
the imem header carries the instruction count and a per-unit count, so every loop bound in the design is runtime data;
A,B,Care flatint8[MAXDIM*MAXDIM]at the fixedMAXDIMstride, exactly asallo_cmp.cdoes for Gemmini (MAXDIM, MAXDIM, 0, MAXDIM);gemm_program(M, K, N)assembles the stream; the RTL never changes.
Verified: 11 programs on one build, all bit-exact – gemm and
gemm.relu at five shapes plus a vector-unit program. Two of those shapes
(12x12x12, 16x16x8) could not be run at all before, because each would have
needed its own accelerator.
What each number’s window contains¶
The audit that withdrew the claims recorded at the foot of this page (Earlier measurements and corrections). Both sides re-measured on the elaborated chipyard tree, not argued from documentation.
The memory models match, so this is not the problem. Gemmini’s harness is
WithBlackBoxSimMem(additionalLatency=0) (AbstractConfig.scala:19 ->
HarnessBinders.scala:122-129), confirmed in the elaborated
TestHarness.sv. That SimDRAM fork only instantiates DRAMSim2 given
+dramsim; otherwise it is mm_magic_t at ~1-2 cycle AXI latency.
Re-running the existing simulator and ELF both ways gives MLP 4.8.8.4 = 1146
without and 1174 with, and dev/records/tinytpu/logs/gemmini_int8_dim4.log records 1146 with no
DRAMSim banner. All five headline shapes are bit-identical either way,
because fill() plus the warm-up leaves A/B/C resident in L1/L2. Both sides
are idealised. Disclosed, not corrected for.
The windows do not match, and that is the problem. allo_cmp.c’s
rdcycle brackets exactly the tiled_matmul_auto call – verified by
disassembling the ELF (rdcycle s4 / jal tiled_matmul_auto /
rdcycle a0). Inside that window sit the tile-size search, padding and
last-tile arithmetic, five config RoCC instructions, the loop_ws
sequence, a closing full fence, and cache coherence for operands the CPU
dirtied immediately before. Ours is ap_start to ap_done with the program
already in DRAM.
Measured on the existing simulator, operands refilled before every window
exactly as allo_cmp.c does:
window |
cycles |
|---|---|
|
1 |
5 configs + |
44 |
5 configs + one hardware |
161 |
full |
536 / 544 (published 574) |
Decomposition of the 574: 44 RoCC config dispatch and fence, 117 actual
accelerator work, ~413 (72%) Rocket software driver. About 30 of that 413 is
L1 coherence from the pre-window fill().
So Gemmini’s accelerator-plus-dispatch fixed cost is about 161 against our
151 – parity, not 3.2x. And because the same driver sits inside all five of
its numbers, the honest pairing at 4x4x4 was 252 against ~161: we were
roughly 1.6x slower, not 2.28x faster (172 against ~161 since e24e433b:
1.07x).
What a real comparison needed was one of two things: a Gemmini window that excludes the driver at every shape, or our number re-measured with an equivalent host-side cost included. The first was the cheaper and it is done – the like-for-like table above is what the 1.55-1.8x result rests on. The second is still open, and is the honest way to measure what a user of each machine would see rather than what each machine’s hardware does.
The memory model, and our sensitivity to it¶
Everything above was measured with -m_axi_latency 0 – a memory that
answers immediately. That is Vitis’s default and it was never stated, so it is
stated here, along with what happens when it is not true.
Rebuilding the design at a given read latency and cosimulating it there
(cosim.py with TPU_AXI_LATENCY; dev/records/tinytpu/logs/cosim_isa_landed_axi_latency.log,
and dev/records/tinytpu/logs/cosim_isa_axi_latency_sweep.log for the pre-landing design):
|
4x4x4 |
16x16x16 |
4x4x4, pre-landing |
16x16x16, pre-landing |
|---|---|---|---|---|
0 (the tables above) |
172 |
686 |
252 |
919 |
16 |
214 (+24%) |
702 (+2%) |
297 (+18%) |
935 (+2%) |
64 |
358 (+108%) |
894 (+30%) |
441 (+75%) |
1127 (+23%) |
Bit-exact at every point (0/16 and 0/256 mismatches). The latency costs the
landed design the same absolute cycles as the pre-landing one to within 3
(+42 / +186 at 4x4x4, +16 / +208 at 16x16x16), so it is a larger fraction of a
smaller total. An earlier revision set these against Gemmini’s end-to-end 574
and 986 as a margin of lead. Those Gemmini numbers include about 395 cycles of
Rocket driver software, so that margin was withdrawn with the headline.
Gemmini’s own harness is also near-zero latency (SimDRAM without
+dramsim), so latency 0 is the matched setting. The sweep is a property
of our design, not a fairness correction.
The large shape amortises latency and the small one does not. 16x16x16
moves 2% at latency 16, while 4x4x4 moves 24% at latency 16 and 108% at
latency 64 (18% and 75% before e24e433b). Latency lands on the fixed term, so a real memory system would
widen the like-for-like deficit above at small shapes.
Counting host overhead symmetrically¶
This page’s window analysis established that a flat ~393-413 cycles of
Rocket driver software sits inside Gemmini’s end-to-end figure, independent of
shape — which is 72 % of the 4x4x4 number and nearly the whole gap between the
two windows. Our own figures are Vitis cosim counts from ap_start to
ap_done, and so contain no host at all.
MiniTPU’s owner supplied the symmetric datum, unprompted, and it is the reason to state this as a methodological rule rather than as a point in our favour: their per-launch cost outside the fabric is about 143 microseconds, which their testbench numbers do not include either.
Note
Corrected 2026-09-23, by its own side. This figure was first given, and first published here, as host work — and most of it is not. Decomposed: ~30 µs is the register path – of which, measured against a no-bus control, only 0.442 µs is the bus and ~29 µs is CPython – and ~52 µs plus 10.8 µs per KiB is the device reloading its own instruction memory (112.7 µs for a 5.62 KiB image). Of 46.76 ms saved by cutting launch count, 81% was the device refetching the same image and only 8.88 ms was host work. It is therefore not comparable to Gemmini’s ~390-cycle software driver at all: theirs is a driver, this is mostly on-device instruction fetch plus bus latency. The asymmetry is real and still large; “they have a slow host” is the wrong lesson.
So the rule for any comparison on this page, and for the benchmark set generally:
Name the window for every figure. What is inside it, where it starts, and where it stops. A cycle count without its window is not a measurement.
Count every machine’s host, or none of them. Counting Gemmini’s driver while omitting our own or MiniTPU’s host time is unfair to Gemmini, and a comparison that omits all of them is fine provided it says so. What is not acceptable is letting one machine’s overhead count while another’s vanishes.
Where both windows are cheap to report, report both. For Gemmini this costs nothing, because the driver-inclusive and accelerator-only figures come out of the same run.
Report an integer invariant beside every timing. Kernel invocations, DMA descriptors, burst iterations, instruction words,
loop_wscalls — some count that cannot drift. A time can always be explained away as noise or a slow clock; an integer cannot, and it distinguishes a changed measurement from a changed machine.The worked example is MiniTPU’s, offered against their own interest. A first measurement of an unchanged commit read a 2x regression, and three things were wrong at once: provisioning had silently reprogrammed the part from a stale firmware directory, replacing the bitstream under the test; the host package was a stale copy missing its committed images, so it rebuilt work per launch; and provisioning had chowned a device node and locked the next user out. What caught it was the launch count — 216 against 180, eighteen a layer instead of fifteen — clock-independent, bitstream-independent and integer, pointing straight at the host. The wall-clock number alone would have sent them looking at the RTL.
Their rule, now ours: read the identity of what you are measuring after provisioning, not only before.
This is the same failure mode as the withdrawn claim recorded at the foot of this page (Earlier measurements and corrections): a number that is true of the measurement but not of the thing being measured. Two of those have now been caught by comparing notes across designs rather than by inspection, which is an argument for continuing to do so.
Gemmini at a matched array size is being built independently by both sides as of 2026-09-22 — deliberately twice, because neither side’s Gemmini figure has ever been reproduced by anyone, and a disagreement between two independent builds of the same nominal configuration would be more informative than either number alone. Reproducing a build means recording the config object, array dimensions, datatypes, scratchpad and accumulator sizes, the chipyard and gemmini commits, the harness, and exactly what is inside the counter window.
The memory-latency knob, and why the sweep outranks the value¶
Our cycle counts come from Vitis cosim, which has no DRAM model:
TPU_AXI_LATENCY is a value we choose, and the published figures are taken at
0. Gemmini’s side is idealised too — WithBlackBoxSimMem(additionalLatency=0)
gives roughly a 1-2 cycle AXI unless +dramsim is used — so neither column
contains a memory system, and that is a disclosure rather than a defect. What
would be a defect is choosing a latency that makes our design look good.
MiniTPU’s owner supplied their equivalent figure with the provenance that makes
it usable: about 40 cycles at 187.5 MHz — 213 ns — for a contiguous
descriptor before its first beat lands, with up to 8 bursts in flight
(tools/round_trip.py in their tree holds it with the derivation). Carry the
seconds, not the cycles: 213 ns is about 88 cycles at our shipped design’s
estimated 2.431 ns period and about 64 at a 3.33 ns target, so converting
through cycles imports their clock along with their memory system.
It is a fitted value, not a measured DRAM latency — fitted so that a simulated schedule change matched a board A/B of the same commit — and its history is the reason this section exists. The value it replaced, 92 cycles, was also inferred, and at 92 their simulator ranked GEMM templates backwards against the board: a 45 % simulated cut in GEMM cycles was 3.6 % on hardware. At 0 it ranked them backwards as well. Two different wrong values, two different wrong rankings, and the simulator was internally consistent and confident in both cases; it was caught only by running the A/B on two boards.
The conclusion to carry, which is theirs and which we are adopting: the sensitivity sweep is worth more than any single value. Ours runs 172 at 0 cycles, 214 at 16, and 358 at 64. So the question a design decision has to answer is not “what is the right latency” but “does my ranking survive the range” — if a variant wins at 0 and loses at 88, the knob chose the design. If conclusions are stable from 0 to 100, the knob does not matter and can be disclosed and forgotten. If they invert somewhere in that range, the inversion is the finding.
And when a ranking does invert, the inversion point in seconds is the interesting quantity, not the inversion point in cycles: seconds say which real memory systems the candidate is good for, where cycles only say which simulator settings it is good for. MiniTPU’s ranking inverted somewhere between 40 and 92 cycles at 187.5 MHz — 213 to 490 ns — and it was the nanosecond figure that let them say their board sits nearer the low end rather than merely that one knob value beat another.
Measured, and it changes what this knob is. The grid was run — shipped
design against the burst-widened candidate at m_axi_latency 0, 16, 64, 88
and 100 — and the candidate’s advantage is exactly -720 cycles at 48x48x48 and
-960 at 64x64x64 at every one of the five points, with the last two predicted
from the first three and returning to the cycle. There is no inversion and no
sensitivity: the knob did not choose the design. The mechanism is that the
widening removes burst iterations (1536 to 96, and 2048 to 128) and the saving
is exactly half of each, so the A and B bursts overlap and only one is ever
critical.
And the knob is non-monotonic — latency 16 beats latency 0, for both
variants. That is decisive about what it is: m_axi_latency is a scheduling
directive to the HLS tool, not a memory latency model. So no row of that
sweep may be read as “what this design does against a memory of that
latency”, and the sweep is not evidence about real memory systems. It bounds
how much this directive can move a conclusion, which is a narrower and much
less interesting claim than the one this page made before the grid was run,
which is kept at the foot (Earlier measurements and corrections).
What survives is the discipline rather than the instrument: report the range over which a conclusion holds, and do not let a knob you cannot interpret decide a design. What does not survive is the idea that our sweep is a better epistemic position than a fitted memory model — it is not a memory model at all, and a fitted one at least attempts the right quantity. Our position is that we have no memory model and should say so.
One caveat on transplanting the number at all: theirs is a ZCU104’s memory system seen through a descriptor-based DMA with two channels and 32-byte beats, on an AXI port that is not on the memory controller’s clock. Our AXI slave is a different design on different silicon. 213 ns is a far better starting point than zero and it is not a measurement of our machine.
The parity baseline¶
A second, named configuration of our design, kept beside the shipped one, whose purpose is to perform at parity with a matched Gemmini by changes that are reasonable rather than contrived. The shipped design is unchanged.
The rules, fixed before any measurement¶
Written and committed on branch gemmini-parity before the first cosim of
any candidate, so the result cannot be fitted to them afterwards.
Parity, defined. At a shape, ours is at parity when our cosim count is at most Gemmini’s median plus its published spread (the
+/-figure on The Benchmark Set, which is the full min-max range over five trials), i.e.ours <= median + spread. Faster meansours < median. Behind meansours > median + spread, and the report names every such shape.Same windows, same shapes, same opponent. Ours is Vitis cosim,
ap_starttoap_done,-m_axi_latency 0, bit-exact, one run per shape (retried once oncycles=None). Gemmini is the matched-array median of five from The Benchmark Set: DIM=4 against T=4 over all ten shapes of that table, DIM=8 against T=8 over all eight of that table, both at MAXDIM=64. No shape is added or dropped after the fact.A mechanism for every change, in one sentence, and an integer invariant reported beside every timing (dynamic issues, per-unit work counts, burst iterations) so a changed measurement can be told from a changed machine.
Synthesisable at the same clock. It must meet the 3.33 ns target with the estimated period reported, and use no FPGA-only structure (no RAM with two write ports) so it also maps to standard cells.
Preference order. (1) a change to the program the generator emits, on the unchanged netlist; (2) the banked burst widening (
TPU_DMA_WIDEN=1); (3) anything else, each with its mechanism and its price.
The configuration, and one command¶
parity-t4 and parity-t8 in parity_sweep.py: the shipped design
with one change, the banked burst widening, at the matched array sizes.
cd examples/tinytpu
TPU_PARITY_CONFIG=parity-t4 python parity_sweep.py # 10 shapes
TPU_PARITY_CONFIG=parity-t8 python parity_sweep.py # 8 shapes
|
4 (against Gemmini DIM=4) / 8 (DIM=8) |
|
64, both sides |
|
16 ( |
|
|
|
32 at T=4; 8 at T=8 |
One csynth per configuration; each (shape) cosim runs in its own copy of that one synthesized solution, so every number in a column is the same netlist.
Reference¶
Reproducing the Gemmini baseline¶
Our side of every table reproduces with one command from a clean checkout,
examples/tinytpu/reproduce.sh, which checks the five cycle
counts (The spec, and holding both consumers to it); TPU_WRAP=1 python cosim.py builds the
old hoisted-argument variant for comparison. The Gemmini side follows.
Every Gemmini number on this page came from a Chipyard tree whose changes were
never committed anywhere: four uncommitted diffs across three nested
repositories, plus a benchmark that was untracked even inside its own
submodule. They were captured 2026-09-18 into
examples/tinytpu/gemmini/.
Pins¶
repo |
commit |
|---|---|
|
|
|
|
|
|
Files¶
file |
applies to |
why it is needed |
|---|---|---|
|
|
|
|
|
|
|
|
the entire end-to-end cycle benchmark. Was untracked, so
|
|
|
the accelerator-only window (below) |
|
same |
the |
|
same |
|
|
same |
adds |
Applying and running¶
G=<repo>/examples/tinytpu/gemmini
cd ~/chipyard && git apply $G/chipyard_RoCCAcceleratorConfigs.patch
cd generators/gemmini && git apply $G/gemmini_CustomConfigs.patch
cd software/gemmini-rocc-tests
git apply $G/roccTests_gemmini_h.patch
git apply $G/roccTests_gemmini_params_h.patch
git apply $G/roccTests_Makefile.patch
cp $G/allo_cmp.c $G/allo_bare5.c bareMetalC/
# build the RTL and run
cd ~/chipyard && source env.sh
cd sims/verilator && make CONFIG=Int8Dim4GemminiRocketConfig -j16
./simulator-chipyard.harness-Int8Dim4GemminiRocketConfig +permissive +permissive-off \
../../generators/gemmini/software/gemmini-rocc-tests/build/bareMetalC/allo_cmp-baremetal
~/chipyard/env.sh activates Chipyard’s own conda environment and so replaces
the allo one; source it in a separate shell (dev/toolchains.rst).
Raw Gemmini output is in dev/records/tinytpu/logs/gemmini_int8_dim4.log.
allo_cmp.c measures what a user gets: tiled_matmul_auto, driver and all.
allo_bare5.c measures what the hardware does: rdcycle -> 5 configs
-> one hardware loop_ws -> fence -> rdcycle, operands refilled by the
CPU immediately before the window. Build it the same way and run it the same
way; a single ~90 s run covers all five shapes. It exists because the
comparison was wrong without it: the difference between the two benchmarks, per
shape, is a flat ~395 cycles, which is what a per-call driver overhead should
look like across a 16x range of work, and is the cross-check that the split is
real.
Things that cost time to learn¶
The stock
matmul/matmul_wstests print no cycle counts – theirread_cycles()calls are commented out upstream.allo_cmp.cexists because of this; do not expect to get numbers from the shipped benchmarks.One Verilator run takes about 80 s, not the 22 minutes an earlier revision of this page claimed – wrong by ~17x, and contradicted by this repository’s own
dev/records/tinytpu/logs/gemmini_int8_dim4.log, which recordswalltime 85.786 s; speed 13.101 us/s. Re-measured: 79.6 s at 14.1 us/s forallo_cmp. So the five-shape sweep is interactive and needs no special budgeting; theallo_bare5.cwindow run quoted above is the same order (~90 s), which is why the two are run together.Stale binaries silently produce a wrong comparison. The 54 binaries found in
build/were int8 artifacts from an earlier elaboration and would have been run against a differently-configured RTL without any error. Rebuild the benchmark whenever the config changes, and checkGEMMINI DIM=in the boot banner –gemmini_int8_dim4.logopens withGEMMINI DIM=4 elem_t_bytes=1, which is the line that proves which hardware produced the numbers.gemmini_counter.hexposes 8 hardware counters that were never read. Roughly an hour of work if a future comparison wants per-unit attribution rather than total cycles.
Which Gemmini configuration is the honest opponent¶
Surveyed from Gemmini’s Scala source on 2026-09-22
(generators/gemmini/src/main/scala/gemmini/GemminiConfigs.scala has ~60
configuration fields). The conclusion is to change almost nothing, and the
reasoning is worth stating because every knob we touched would invite the
question of whether we tuned the opponent to lose.
The four axes pull in different directions and are settled differently:
Array size — match it. The only axis where a mismatch is a pure multiplier on peak throughput, 16x for 16x16 against 4x4, with no counterargument. This is why stock
GemminiRocketConfigis not a usable opponent.Datatype — match it, and we do. int8xint8 into int32 on both sides, which is Gemmini’s own default. bf16 would be a step away from matched, not toward it.
Memory system — cannot be matched; enumerate it. Gemmini is Rocket with a 32 KiB L1 D$, an L2 and
WithBlackBoxSimMem(additionalLatency=0); ours is Vitis cosim at zero AXI latency. Both idealised, neither is the other’s memory, and equalising them means rebuilding one side’s SoC.Dispatch path — match it by *window*, not by configuration. No knob addresses it; only the window does, which is exactly what fixed the withdrawn claim recorded at the foot of this page (Earlier measurements and corrections).
So the config delta that survives review is three fields, two of which are
the array size — meshRows and meshColumns — with the third being
has_training_convs = false. (tileRows and tileColumns also appear in
the copy(), restated at the values they already hold, so a reviewer counting
changed fields finds three.) has_training_convs is read in exactly one
place in the whole generator, six conv-loop flags in LoopConv.scala, and
nowhere in LoopMatmul, the mesh, the scratchpad or the header generator — so
a GEMM provably cannot observe it, verifiable in one grep. Everything else is
named as unexercised. Two disclosures rather than fixes: dataflow =
Dataflow.BOTH leaves Gemmini carrying an output-stationary datapath we never
use, which favours it if anyone, so keeping it is the conservative choice; and
a DIM=16 taken from stock would also flip has_training_convs to true,
making the matched points differ in a second field.
A second matched point is being added at DIM=8, because T is now a working
parameter on our side and a single matched point cannot separate “our design
is slower” from “our design is slower at this one size.”
The capacity asymmetry is cycle-neutral, and this is the finding¶
The obvious objection to this comparison is memory capacity. Gemmini has a 256 KiB scratchpad and a 64 KiB accumulator; ours are smaller by one to three orders of magnitude, depending on which of our builds is meant — and the distinction matters enough that an earlier revision of this page got it wrong.
Warning
Corrected 2026-09-22. This section used to say we have “a 4 KiB
scratchpad, 4 KiB of vector registers and a 2.1 KiB accumulator”, and put
the asymmetry at 64x and 30x. Those figures are ``MAXDIM=64``. The
memories are derived from MAXDIM, so at the ``MAXDIM=16`` baseline —
the build every published cycle count and the published 1,136,598 cell area
belong to — the scratchpad and the vector registers are 64 rows of 32 bits
each, 256 bytes apiece, and the accumulator is 2.0 KiB. Measured out of
the shipped RTL, not read off the source:
variant |
scratchpad |
vregs |
accumulator |
all RAM modules |
|---|---|---|---|---|
|
0.25 KiB |
0.25 KiB |
2.0 KiB |
3.50 KiB |
|
4 KiB |
4 KiB |
2.1 KiB |
18.62 KiB |
|
4 KiB |
4 KiB |
4.25 KiB |
20.75 KiB |
So the asymmetry against the baseline is 1024x the scratchpad and 32x the accumulator, not 64x and 30x. The “all RAM modules” column adds the DMA read buffers and the sequencer’s small RAMs, which are storage but not operand storage. The cycle-neutrality finding below is unaffected — it is a statement about Gemmini’s tiling search, not about our capacities — but the area argument in Area: the axis this page was missing rests on these numbers and uses the corrected ones.
The asymmetry does not buy Gemmini a single cycle at any published shape. Re-running
tiled_matmul_auto’s own tiling search across capacities at DIM=4 gives the
loop_ws call count below; every row is legal under the config’s require
clauses:
Scratchpad / accumulator |
4x4x4 |
16x16x16 |
32x32x32 |
64x64x64 |
64x32x64 |
|---|---|---|---|---|---|
256 / 64 KB (what we ship) |
1 |
1 |
1 |
1 |
1 |
64 / 32 KB ( |
1 |
1 |
1 |
1 |
1 |
32 / 8 KB |
1 |
1 |
1 |
4 |
4 |
8 / 4 KB (the area build) |
1 |
1 |
4 |
12 |
12 |
4 / 4 KB (the floor tested) |
1 |
1 |
4 |
32 |
12 |
Every capacity from 256 KB down to 4 KB issues exactly one loop_ws from
4x4x4 through 16x16x16, so shrinking Gemmini’s memories would not move any of
the five published numbers. That is what licenses the 8 / 4 KB elaboration the
area comparison uses. The asymmetry is an area disclosure, not a cycle
correction — which is a far stronger position than “we could not match it”.
The binding limit is always the accumulator’s half capacity, never the
scratchpad.
(The two lower rows used to be labelled “~2x ours” and “~matched to ours”.
They are neither: against the MAXDIM=16 baseline’s 0.5 KiB of operand
storage, 8 KB is 16x and 4 KB is 8x. Against T8_MAXDIM64 the 8 / 4 KB row
is matched, to 2 %.)
It stops being neutral at exactly 32x32x32, so as shapes grow this argument needs restating rather than reusing. Keeping Gemmini’s own default capacity is also the choice that avoids hand-rolling a multi-tile nest and then arguing about whether our tiling was fair.
Limits and known failures¶
The measurement noise floor, which constrains every claim on this page¶
The same reproduction exposed something that had been visible in the recorded numbers all along and never priced: the trial-to-trial spread reaches 20 cycles at a shape whose total is 161 — identical hardware, identical binary, consecutive runs. That is up to 12 % noise at the small shapes, and it comes from each measured window’s sensitivity to the preceding call’s cache and scratchpad state.
Two consequences, both binding:
No single-trial number at 4x4x4 or 8x8x8. Report a median and the spread. A 1.07-1.24x deficit measured at those shapes is, at the small end, partly inside the noise. It does not erase the result — the sign is consistent across all five shapes, and that consistency is the evidence — but any claim of a difference smaller than the spread is unsupportable.
The noise is the Gemmini column’s, not ours. This spread comes from a full SoC — Rocket, an L1 cache, a scratchpad whose state the previous call left behind. Our own figures are Vitis co-simulation of a fixed design on fixed inputs, which is deterministic: the five shapes reproduce exactly, run after run, and the alignment variant’s independent re-measurement reproduced every one of its numbers to the cycle. So repeats are needed on the Gemmini side and are not needed on ours, and a design A/B measured only by our cosim — the latency sensitivity grid above, for instance — does not need them either. A cross-machine comparison is still limited by the noisier side of it.
That claim was incomplete, and the correction is measured. A controlled count of nine runs of one configuration gives 8 successful co-simulations, 8 identical cycle counts, 0 differing counts — and 1 run that yielded no number at all (
cycles=None, no TB lineat one shape while another returned its usual figure in the same run). So the statement has two halves and only the first was being made: the simulation is deterministic; the pipeline can fail to produce a number. A nondeterministic simulation gives a different number; a flaky pipeline gives no number.The operational consequence is the part that changes behaviour: a single rejection by a cycle gate is not evidence and must be retried rather than believed. Repeats are still not needed to establish a value on our side — that is what the 8-and-0 count says — but a failure on our side is not a measurement until it has been seen twice.
This is the cycle-domain analogue of the synthesis noise floor recorded in MiniTPU: Reference Architecture — two builds of an identical netlist differing by 1,407 LUT and 0.046 ns. Both say the same thing: state the noise before stating the difference, and say which measurement the noise belongs to.
Verify determinism once per configuration rather than assuming it. One extra run per configuration — not per data point — and confirm the counts are identical. The reason is not sampling: it is that a simulator figure which turns out not to be deterministic would undermine every comparison drawn from it, and that is much better discovered in a two-run check than in a disagreement with someone else’s table hours later. Our cosim has effectively passed this already, through the alignment variant’s independent re-measurement reproducing every number to the cycle, but a new configuration has not.
And the trap on the far side of determinism, which is the more dangerous one: a simulator number can be perfectly reproducible and still wrong about hardware. Ours contains no memory system at all, and MiniTPU’s contains a fitted one — the 92-cycle failure recorded above is exactly this, a confident, repeatable simulator that ranked designs backwards against the board. Reproducibility is a property of the measurement; agreement with hardware is a separate claim needing separate evidence.
Do not mix the two benchmarks’ columns¶
There are now two window benchmarks, and they do not produce interchangeable latencies: the newer steady-state file reads 2 to 14 cycles lower at the same shapes on the same hardware. The cause is the call sequence rather than the hardware — the files differ in how many trials they run per shape and in what runs between them, and each measured window is sensitive to the preceding call’s cache and scratchpad state.
So a table must say which file produced it, and a comparison must take both columns from the same file. This is the same class of error as the window problem that produced the withdrawn claim: a real measurement, correctly taken, that means something different from what it is being used to mean.
A third hazard for anyone reproducing at another array size¶
Beyond the in-place gemmini_params.h rewrite and allo_bare5 missing from
the patched tests list: rocc-tests at its pinned HEAD ships a header that
no stock configuration generates. It carries DIM 16 with 4096 bank rows and
1024 accumulator rows but no ACC_READ_FULL_WIDTH, while the default config
sets acc_read_full_width = true and the generator emits that macro — so the
committed header came from a leaner config than the one it appears to describe.
A build that reuses the committed header instead of regenerating it will differ
from a build that regenerates, for a reason invisible in the Scala.
One hazard, again¶
Elaborating these two configurations rewrote gemmini_params.h twice, as
every elaboration does. The header was restored to the committed DIM=4
snapshot afterwards and verified by checksum. The exporter does not trust that
header at all: it reads DIM back out of the scratchpad’s own geometry in
.top.mems.conf — a bank is DIM int8 elements wide, an accumulator row is
DIM int32 — and refuses an export whose RTL does not match the
configuration it claims. The C benchmark build still depends on the header, so
the snapshot-and-restore step is still required for anyone re-running cycles.
Open: ex_accumulate in the window benchmark¶
gemmini/allo_bare5.c passes ex_accumulate as a literal true, where
the real driver computes !no_bias || D == NULL, which is false for the
no-bias calls we make. In hardware it sets a bit on the preload’s accumulator
address rather than issuing extra commands, so it adds no RoCC traffic, but it
turns the k=0 accumulator writes into read-modify-writes and is therefore not
guaranteed cycle-neutral.
Measured both ways, and settled: the published figures stand. The
driver-matched false makes Gemmini 0 to 4 cycles faster at the five
shapes — so our deficit was very slightly understated, in the direction that
does not flatter us. The effect is below the measurement’s own noise: the
true column’s trial-to-trial spread reaches 20 cycles at one shape, larger
than the effect itself. Its sign is consistent across all five shapes, so it
is a small systematic effect under the single-trial noise floor rather than
noise, and new work should use the driver-matched form. 161 / 220 / 347 / 391 /
593 need no revision on this account.
Two operational hazards found in the same survey, both of which have silently
produced wrong comparisons before: every elaboration rewrites
gemmini_params.h in place, so elaborating a second configuration
destroys the first’s header and the next C build silently targets the wrong
hardware — snapshot it, rebuild immediately, and check the GEMMINI DIM= boot
banner. And the committed roccTests_Makefile.patch adds only allo_cmp to
the tests list, not allo_bare5, so build.sh will not build the
window benchmark; it needs its explicit make target.
Power: declared absent, with the reason¶
This project’s own stated principle is that informed co-design needs timing and power across an array of architectures, so power’s absence here is a real gap in the judge, and it is declared rather than papered over.
No power number from this flow is publishable, and none will appear in any
table on this page. DC’s report_power output is a default-toggle-rate
estimate with no activity data. An earlier run’s 57.1 mW was recorded and has
already been withdrawn once; it must not travel again, in any row, however
marked.
What closing the gap would actually take, and why it is not worth doing for this evaluation:
level |
cost |
what it still would not tell you |
|---|---|---|
RTL switching activity |
1-2 days of bring-up: scope a VCD to |
the two SAIFs describe different windows — ours is |
gate-level activity |
the above, plus simulating the DC netlist against the NanGate45 Verilog models with the cosim testbench, which is C++ and drives AXI — a real bring-up on our side and a new harness on Gemmini’s |
still no wire capacitance |
the honest version |
SRAM macros instead of flip-flop memories, and place-and-route |
nothing — but it replaces the area methodology, so every published figure on this page would have to be re-run |
The blocker is not effort, it is that under sram_mode='none' and without
P&R a power number is dominated by two artefacts larger than the effect being
measured. First, clock power into flip-flop arrays: 98,304 flops of
Gemmini memory and 200,561 sequential cells of ours are toggled on every edge
that is not gated, and a real SRAM’s dynamic energy per access is nothing like
a flop array’s — the same objection that makes the total-area column fragile,
except power is more sensitive to it, not less. Second, no interconnect:
with no P&R the wire capacitance is a wire-load-model guess, and at 45 nm
interconnect is a large share of dynamic power.
Recommendation: do not pursue power for this evaluation. The paper says power is absent, and says this is why. That is a stronger position than an indicative number quoted as a measurement.
Stated precisely, because a parallel effort is producing switching-activity files and this must not read as an argument against it: there are three objections to a power number from this flow, and activity annotation removes exactly one of them. It removes the default-toggle-rate objection, which is the one that makes the current figure unpublishable outright, and it is worth having on its own terms. It does not touch the flip-flop-memory artefact or the missing interconnect. So an annotated number from this flow would be compromised rather than meaningless — a real improvement, and still not a measurement of either design’s power.
That is compatible with the methodology commitment in dev/paper_outline.md
(publish power only if both sides are activity-annotated, otherwise report
none and say why): if only one side is annotated the answer is no on that
ground alone, and if both are, the answer is still no on these two. The
cheapest credible energy axis is SRAM macros plus place-and-route — the same
prerequisite that would replace the area methodology. If the project later
wants energy, that is the order to do it in: macros first, because they fix the
area artefact and the power artefact at once.
The capability gap, which is worse for us than the cycle deficit¶
Gemmini runs 128x768x768 today, through tiled_matmul_auto’s own tiling
search. We cannot address a matrix that size at all: our operands are a single
int8[MAXDIM*MAXDIM] region, so the shape has to fit the addressable space
rather than being tiled into it. Unblocking it needs a runtime base and stride
on the DMA load and store paths, a fifth loop level, and possibly a fourth
address term.
This matters more than the 1.09x. A 9 % cycle deficit at a shape both machines can run is a tuning result; being unable to express the shapes a real workload uses is a capability result, and no amount of cycle-level work closes it.
Results and history¶
Every measurement this page’s method produces, and every claim it has withdrawn, are on TinyTPU-isa vs Gemmini: Results and History: the like-for-like table, the marginal-cost sweep, the area and frequency comparison, the parity baseline’s results at T=4 and T=8, the independent reproduction, and Earlier measurements and corrections.