MiniTPU: Reference Architecture¶
MiniTPU is the reference architecture the TinyTPU-isa work benchmarked against (see TinyTPU-isa vs Gemmini: A Matched Comparison). Originally titled “MiniTPU – reference architecture we benchmarked against”.
Not our design, and not ours to modify. MiniTPU is another engineer’s
machine; its source is ~/core/minitpu (live) and ~/core/npu (read by an
earlier pass, now empty), and neither travels to zhang-21. This records what we
found about a reference architecture we benchmarked against, because the
findings outlive the tree. Everything was read-only. Distilled from two rescued
scratchpad reports; (verified) means re-checked against ~/core/minitpu on
2026-09-18.
An Allo model of this machine’s compute core lives at examples/minitpu
(python examples/minitpu/run.py). It is component-for-component with the
RTL and says nothing about cycles. Its README lists both where the model
diverges from the machine and, separately, where this page disagreed with
the source. Reading the RTL for that model found nine such places; each is
corrected below and marked CORRECTED 2026-09-24.
1. Shipped-bitstream QoR (xczu7ev-ffvc1156-2-e, Vivado 2023.2)¶
Every figure here is a board measurement of a named bitstream, reported by MiniTPU’s owner. Two builds are kept, because §2’s noise-floor argument needs two to compare; the bitstream id is part of the figure.
value |
note |
|
|---|---|---|
CLB LUT |
194,459 (84.4 %) |
LUT is the scarce resource, not DSP |
CLB registers |
123,294 |
|
DSP |
838 |
|
WNS |
+0.053 ns at 200 MHz |
|
Power |
7.880 W on-chip (vectorless) |
|
GPT-2 body throughput |
160.60 tok/s at 32 rows, 158.04 at 256 — medians of five runs, 2026-09-22. 28.4 GFLOP/s, 29.6 % of the 96 GFLOP/s array peak (was 24.2 %). |
Confirmed on a second board: .189 measured 160.43 and 158.40 on the same commit, agreeing within 0.4 %. Supersedes the earlier 79.16 / 129.70 pair — the hardware did not change; see “The concession that became the win” below. |
Kernel checks |
17/17 GPT-2, 6/6 Qwen |
batch invariance bit-identical for |
value |
source |
|
|---|---|---|
CLB LUT |
191,231 (83.00 %) |
|
CLB registers |
118,684 (25.76 %) |
same |
DSP |
838 (48.50 %) |
same |
BRAM tile |
147 (47.12 %) |
same |
URAM |
15 (15.63 %) |
same |
WNS |
+0.114 ns on |
|
Power |
8.255 W on-chip (vectorless) |
|
BRAM and URAM were not re-reported for 0xB1FF21F4; the earlier build’s
figures are the only ones on record for them, and are not restated as current.
Every board number was taken at 187.5 MHz, not the 200 MHz the bitstream is
constrained to. PL0 is set by hand (scripts/set_pl_clock.sh,
PL0_REF_CTRL = 0xFF5E00C0); without it the PL runs at 99.999 MHz and every
check still passes, and clk_summary misreports it.
tools/dma_fence_report.py hardcodes CLOCK_HZ = 187.5e6. Any cycles→seconds
or GFLOP/s comparison must use 187.5 MHz; 200 MHz overstates it by 6.7 %.
A stale README power figure we reported, and the value it was corrected to, are in Earlier measurements and corrections.
2. The QoR noise floor — the most reusable finding here¶
Two builds of an identical netlist differed by −1407 LUT and −0.046 ns.
So in this flow no QoR delta below roughly 1.5k LUT (~0.6 % of the device) or 50 ps is evidence of anything — place-and-route seeding alone covers it. That is a fact about the tool flow, not about MiniTPU, and it applies to us:
It is the standard behind commit
644d8cdc: the accumulator rotation bought 2.3 % end-to-end for 13.7× the flip-flops in one unit — cost far outside the noise band, benefit barely outside it. Reverted.Every future Catapult PPA claim needs a delta above this band, or repeated builds with different seeds. A single-build A/B below it is a coin flip reported as a result.
Provenance: the ±1407 pair came from rescued build logs that did not survive —
the nine ~/core/minitpu* worktrees as of 2026-09-18 all reported 191,231 /
+0.114 ns (the 0xB182E69D build in §1), so it cannot be re-derived. The
magnitude is the load-bearing part.
3. Architecture¶
The shape of the machine, read out of ~/core/minitpu/src/ at the same HEAD
as the (verified) rows below. Parameter names are the RTL’s; the values are
the shipped configuration (NUM_LANES = 16, DATA_WIDTH = 16,
NUM_VREGS = 32):
host (AXI-Lite) -> command unit -> IRAM loader -> IRAM
|
v
+--------------------------------------------------------------+
| sequencer fetch; STACK_DEPTH = 4 loop stack; LB_CAP = 24 |
| bundle loop buffer; stalls only on DELAY, |
| matrix-busy and halt-drain -- no interlocks |
+--+--------------+--------------+-------------+--------+------+
| V | M | MEM | S | C
v v v
+----------+ +------------+ +-----------+
| VPU | | MXU | | vld / vst |
| ALU, SFU | | DIM x DIM | | vmemld / |
| xlu | | = 16 x 16 | | vmemst |
+-----+----+ | BF16 PEs, | +-----+-----+
| | MXU_ACC_W | |
| | = 24 bit | |
| +-----+------+ |
| | per-lane |
| | output FIFO |
| v |
| +---------------+ |
+----->| VREG file |<-----+
| 3R1W, 32 regs |
| ONE write | <-- the measured bottleneck,
| port | not the array
+-------+-------+
|
v
+---------------------+ +-----------+
| VMEM, one flat word | | DMA, |
| array; compute port |<----->| 2 async |<--> DRAM
| and DMA port, whole | | channels |
| words only | +-----------+
+---------------------+
One VMEM word is one VREG: NUM_SUBLANES * NUM_LANES * DATA_WIDTH = 128 B,
which the DMA names as NUM_SUBLANES = 4 beats of 32 B. That is the only
role NUM_SUBLANES plays in addressing – see the VMEM correction below.
Array |
16×16 weight-stationary BF16 PEs; |
Accumulator |
24-bit float per PE (sign + 8 exp + 15 frac, |
Bundle |
128-bit VLIW: slots V / M / MEM / S / C, shared immediate,
7-bit DELAY ( |
Hazards |
none in hardware. Fetch stalls only for DELAY, matrix-busy, halt-drain; VREG
RAW, write-port collisions, WAW, SREG RAW, VMEM ownership are the assembler’s
job ( |
Loops |
4-deep hardware stack, one |
Replay |
24-bundle loop buffer ( |
AGU |
1-term, shift-only: |
Fences |
|
IRAM |
self-fetching: host loads bundles over AXI-Lite via a command unit and IRAM loader, then the core fetches for itself |
Silent limits |
MXU output FIFO 32/lane — overflow drops results, no replay; loop buffer 24;
DELAY width. Checked by |
Note
One row above no longer matches the tree. “MXU output FIFO 32/lane” is
the 2026-09-18 reading, and §7 records both source trees agreeing on 32.
vpu_pkg.sv now defaults MXU_OUTPUT_FIFO_DEPTH to 64
(MINITPU_MXU_OUTPUT_FIFO_DEPTH), and the parameter is the thing that
must match docs/isa_latency.json or the board overflows it silently.
Stated rather than restated, because this is another team’s design and
their tree is the record: the depth to quote is whatever
MINITPU_MXU_OUTPUT_FIFO_DEPTH is at the commit being discussed.
CORRECTED 2026-09-24, on the unit as well as the number. The depth
counts result rows, not entries: mxu.sv:114-117 keeps 64 rows as
OUTPUT_GROUP_ENTRIES = 16 entries of OUTPUT_GROUP_W = 64 bits, each
entry a group of NUM_SUBLANES = 4 BF16 results, with an elaboration
$error on non-divisibility. The rule a schedule needs is four rows per
vmatpush, so at most 16 pushes outstanding (asm.py:933-955).
“32/lane” was wrong in the number and in the unit.
The bundle’s fixed bit layout, from sequencer_pkg.sv’s
encoded_bundle_t, which is declared MSB first so that the struct is the
layout (cells not to scale; BUNDLE_WIDTH = 128, of which 113 bits carry a
slot):
+-------+-------+-------+-------+-------+-------+-------+-------+
| V | M | MEM | S | C | IMM | DELAY | rsrvd |
|127:108|107:100| 99:66 | 65:53 | 52:46 | 45:22 | 21:15 | 14:0 |
+-------+-------+-------+-------+-------+-------+-------+-------+
bits: 20 8 34 13 7 24 7 15
IMM_W = 24 is the one shared constant – S’s immediate, loop.begin’s
payload, or a descriptor’s displacement – and exactly one slot may claim it
per bundle; encode_bundle fails the build if two do, because the losers
would silently read the winner’s constant. DELAY_W = 7 holds issue 0..127
cycles, sized so one field covers the MXU’s result latency. (That latency is
85, not the 82 this page carried — CORRECTED 2026-09-24: 82 survives only
in a stale comment at sequencer_pkg.sv:32, against 85 in
docs/isa_latency.json and docs/ISA_AND_INTERFACES.md:57. DELAY_W = 7
covers either.)
Why BF16: accumulator width was chosen first from the LUT/DSP budget, and an
8-bit-exponent format is what fits it — “there is no FP32 state anywhere in this
design… fp32’s extra 8 bits could only ever hold zeros there”
(minitpu_config_pkg.sv). Why no interlocks: “a legal schedule is a correctness
argument, and the assembler carries it” (README) — paid for with two silicon bugs
nothing caught at runtime.
VMEM is one flat array, not four banks per lane¶
The rescued scaling report describes VMEM as 4 banks per lane
(bank = row mod NUM_SUBLANES, conflict cost gcd(stride,4), an assembly-time
conditional latency) and stakes its whole staged plan on it. Wrong for the
live design. src/core/vpu/vpu_word_array.sv is a single flat dual-port
URAM array — one compute port, one DMA port, whole words only — with no
arbiter and no interlock; its own comment: “same-word collisions across the
two ports are undefined, as on the FPGA” (verified). No banking, no conflict
cost model, nothing to model as a conditional latency. Do not build against the
banked model. VMEM is 4096 words of 128 B — 512 KiB — one word being exactly
one VREG (vpu_pkg.sv:88-92).
CORRECTED 2026-09-24 — the banking is real, it is just not in VMEM
This section reads as though “4 banks per lane” were a fiction. The phrase
is in the live tree: NUM_SUBLANES = 4 is commented “== banks per lane”
(vpu_pkg.sv:45), and the vector register file really is four banks —
vpu_vreg_stripe.sv:73-87 instantiates NUM_SUBLANES copies of
vpu_regfile, each 256 bits wide and 32 deep, because every lane of a
sublane shares the broadcast VREG address. What the rescued report got
wrong is which structure is banked and what it costs: the VREG banks are
not a conflict source (one address, all four banks, per access), and VMEM —
the thing it built a gcd(stride,4) latency model on — is flat. Keep the
conclusion; drop the implication that nothing in this machine is banked.
4. Measured performance, and the bound that is not a measurement¶
Cycle model — a simulator number from MiniTPU’s own
tools/gemm_cycle_model.py, not a board figure; n output column tiles,
B contraction blocks of 32. The current fit, per MiniTPU’s owner,
re-measured over 20 points with maximum residual 0:
cycles = 46 + 281·n + 64.5·B + 111·n·B
The fit this page carried before the vmatpop occupancy correction
(72c8b78) is kept in Earlier measurements and corrections.
The matrix step is 52 cycles, and the bottleneck is a port of the 3R1W vector-register file — port C, shared by store and matrix — not the array (from MiniTPU’s owner).
CORRECTED 2026-09-24 — port C is a READ port, not the write port
This paragraph said “the write port … port C”. Port C is one of the three
read ports (raddr_c, vpu_regfile.sv:16-27); its two readers are
the store path’s data read and the matrix stream engine, and
vpu.sv:429-431 is a fixed-priority mux the matrix wins, with no
interlock and no stall. The single write port is a different structure:
an OR-mux over six sources (vpu.sv:289-305) whose only guard is a
$onehot0 simulation assertion. Both are real contention points and a
cost model needs both — tools/gemm_cycle_model.py’s step_bounds()
books the write port separately from the stream engine.
19.0 % of peak is the MEASURED marginal efficiency, from their own RTL sweep, and it does not move with size.
36.4 % of peak is also a MEASUREMENT, of a different quantity – whole-model utilisation, arithmetic performed over array peak, on
gpt2-124m.prefill.offline.b1t256: 1,104 launches, 1.315 s wall, 194.67 tok/s, 34.91 GFLOP/s against a 96.0 GFLOP/s array peak, PL busy 92.8 %, a board measurement at 187.498 MHz, five repetitions, wall spread 0.15-0.79 %. It says nothing about controllers.
CORRECTED 2026-09-24 – 36.4 % is a measurement, not a bound
This page, and TinyTPU-isa vs Gemmini: Results and History, presented 36.4 % as a structural ceiling set by “the matrix controller not overlapping a push with a pop”. It is nothing of the kind. Per MiniTPU’s owner it is the utilisation of one workload, and the neighbouring rows of the same table read 36.1 %, 34.7 % and 32.6 % at other shapes – a workload figure moves with the workload; a structural ceiling does not. The supposed mechanism does not exist either: push and pop run on separate engines (see below). Do not quote 36.4 % as a bound, a ceiling or a limit. The warning that used to sit here – “never present the bound as a measurement” – was exactly backwards.
A 16×16×16 matmul takes 168 cycles (originally from the rescued comparison work and not stated in either surviving source doc; MiniTPU’s owner now reports the same 168 for the 16×16 GEMM Verilator workload, so it is corroborated).
The rest of the gap is result latency — 85 cycles from vmatpush to first
result — and it cannot be hidden by reordering: loading the next weights
early corrupts the tile in flight. Splitting the matrix controller alone was
measured and gains nothing.
CORRECTED 2026-09-24 — the commit is skewed, and the second bank exists
Two errors in the sentence this replaces. (a) Weight commit does not
reach every PE on one cycle. It enters the west edge per row skewed by
row * MXU_PE_LATENCY (mxu.sv:230) and then walks east one PE per
cycle, so PE (r, c) commits at r*4 + c; the span is
WEIGHT_SWITCH_SPAN = (DIM-1)*PE_LATENCY + (DIM-1) = 75 (mxu.sv:60),
and the switch is armed not by the load ending but by the next activation
(tile_starts, mxu.sv:170). (b) A second weight bank is not hardware
still to be built — it is already there. MXU_WEIGHT_BANKS = 2
(vpu_pkg.sv:108), with an elaboration $error if it is ever not 2,
because the bank pointers are literal toggles. The 75-cycle rule is the
two-bank contract: a vmatload refills the bank of the load two before
it, and issuing it sooner overwrites weights a PE has not switched to —
wrong results, no fault (mxu.sv:246-262, asm.py:1313-1330).
Board-validated means exactly three bring-up kernels: halt 4 cy,
vreg 12 cy, scalar 8 cy. Nothing else on this page is a board cycle
count.
The workload figures below are Verilator counts, not board figures. Per
MiniTPU’s owner, after its commit 72c8b78 corrected vmatpop occupancy
from 7 to 4, 16×16 GEMM is 168 cy and 16×16 GEMM→GELU is 155 cy. The
remaining Verilator targets are unchanged on record: 32×64 BF16 add 119 cy,
32×64 multiply-add 155 cy, 32×32 DMA-streamed multiply-add 929 cy, 768-element
LayerNorm 941 core cy / 218 bundles.
Accuracy: GPT-2 124M block 0.68 % vs fp32, Qwen2.5-0.5B decoder layer 3.81 %.
B = 1..16 is bit-identical row-wise only.
What this page said about all three before it was corrected is in Earlier measurements and corrections.
5. What this says about our own cost model¶
docs/isa_latency.json (in the MiniTPU tree) is the measured-from-silicon version of what
tpu.latency(unit, ii=, depth=) asserts pre-synthesis, and carries two latency
shapes our UnitLatency (allo/exp/dsa/core.py:438, on chia-codesign; see ACT and the TinyTPU-isa Mapper) does not express:
conditional latency (latency depending on a statically known operand
property — in the npu tree vld’s L = W + conflict degree; per the correction
above the live design has no such case), and occupancy ≠ result latency
(vmatpush occupies the controller 5 cycles, first result at +82 — queue
semantics, not an (ii, depth) pair).
Writeback offsets for scale: ALU 5, SFU 7, cross-lane reduction 15 (it crosses
all 64 elements — a reduction tree, not a per-lane pipe), vld 6, vmatpop
3..6. Occupancies: vmatload 18, vmatpush 5, vmatpop 4 (was 7
here; corrected by MiniTPU’s commit 72c8b78).
Also worth carrying: the binding resource is the instruction encoding, not the silicon. The compute bundle has 1 spare bit — doubling VMEM rows per slot fits exactly it; doubling VREG count needs 5 bits that do not exist.
6. Defects reported outward to that design’s owner¶
The stale 7.255 W README figure (shipped: 8.255 W) — since fixed.
src/core/mxu/mxu_adapter.svwas reported outward as dead code: listed incore.fandvpu.f, instantiated nowhere. CORRECTED 2026-09-24: the file does not exist.grep -rn mxu_adapteroversrc/,tb/, the.ffilelists and the build scripts returns nothing, and there is no such file anywhere under~/core. The MXU/VPU boundary shims aremxu_serializer/mxu_deserializer, both pass-through wires in one file (mxu_serializer.sv:14,27-28). The rescued report treating the MXU adapter path as live is wrong twice over: the path is not live, and the file is gone.A stale
matrix_busy_odoc comment.
7. Where the two source trees disagree¶
The rescued report was written against ~/core/npu; ~/core/minitpu is live
and has moved. Do not mix vocabularies:
rescued report ( |
live |
|
|---|---|---|
VLIW slots |
E / M / X / S / D / F |
V / M / MEM / S / C |
DELAY field |
6 bits |
7 bits |
VMEM |
4 banks/lane + conflict model |
flat dual-port URAM, no arbiter |
Both agree on: 16×16 BF16 array, 24-bit accumulator, 128-bit bundle, no interlocks, 24-bundle loop buffer — and on two figures they both get wrong, a 32-entry MXU output FIFO and an 82-cycle push→result latency. CORRECTED 2026-09-24: 64 result rows per lane, and 85 cycles; see §3 and §4. Two reports agreeing is not verification when both descend from one reading.
Consequence: the rescued report’s 8-stage TinyTPU scaling plan rests on the banked-VMEM model and a live MXU adapter, so it is invalidated in those parts. Not reproduced here; what survives of it is §5.
The concession that became the win¶
The rule first, because it is the part that transfers: an overhead you exclude from a window is an overhead you cannot see, so put the excluded quantity beside someone else’s – the comparison is what makes an absurd value look absurd. What follows is the episode that established it: the most instructive thing to come out of comparing two designs, and a method finding rather than an architectural one.
While establishing that a fair comparison must count every machine’s host or none of them (TinyTPU-isa vs Gemmini: A Matched Comparison), MiniTPU’s owner volunteered a figure against their own interest: their per-launch cost outside the fabric is about 143 microseconds, none of which their simulator numbers include. It was offered so that a comparison counting Gemmini’s ~390 cycles of driver would not quietly omit theirs.
What that cost actually is — corrected 2026-09-23 by its own side. It was given, and published here, as host work. An ASIC-readiness audit decomposed it into ~30 µs on the register path and ~52 µs plus 10.8 µs per KiB for the device to reload its own instruction memory — 112.7 µs for a 5.62 KiB GEMM image. Of 46.76 ms saved by cutting launch count at M=256, 81% was the device refetching the same image into IRAM; host work was 8.88 ms. So it is not comparable to Gemmini’s software driver, and the fix with the largest measured value is a per-program IRAM base register letting resident images skip the reload, worth 96.7% of launches.
Corrected again, 2026-09-23, same day. The ~30 µs register path was first
attributed to AXI-Lite bus latency. Measured against a no-bus control —
identical Python driving a plain bytearray instead of the memory-mapped
device — the bus is 0.442 µs and the other ~29 µs is CPython. A posted
write costs nothing measurable. This is worse for their side than the version
it replaces: interconnect latency is inherited and a die would remove it,
whereas the driver is a choice and a die would not. The structural fix they had
planned for this path is consequently rejected — it cannot recover more
than 0.44 µs, and its own cache-maintenance write costs more than everything it
removes.
Two methodology notes from that measurement, both general. Timing single
operations was the original error: one perf_counter() call costs ~1.7 µs
on that board’s ARM, more than most of what it would time, and the tell was
that the no-bus control measured slower than the real thing. And a control
that shares everything but the mechanism under test is what made the
attribution legible at all; without it, Python would have gone on being
recorded as interconnect indefinitely.
The parallel to our own design is worth stating: instruction supply dominates both machines, in different currencies. It is 60.0% of our silicon area and, here, the majority of their per-launch time.
Stating it next to Gemmini’s ~390 is what made it absurd rather than normal — two orders of magnitude, for the same job. Within hours, two commits on their master attacked exactly that cost, reading a configuration file once instead of once per launch and spinning past the slowest launch, and took the GPT-2 body from 79.16 to a claimed 160.38 tok/s on the same bitstream. Neither cost was visible to their simulator, which is why both survived as long as they did.
So the fairness adjustment was a diagnostic. The honest accounting did not merely make the comparison defensible; it located the largest measured win of that night, in a quantity nobody had been optimising because nothing in the measurement setup showed it.
The transferable form, and the reason this is recorded on our side too: an overhead you exclude from a window is an overhead you cannot see. Our cosim figures contain no host at all — not a fast host, none — so we have no equivalent number to be shocked by yet, and a real deployment eventually will. The rule that follows is not only “say what your window excludes” but “put the excluded quantity beside someone else’s, because the comparison is what makes an absurd value look absurd.”
What aligning to MiniTPU’s semantics costs¶
An experiment, now stopped, asked whether TinyTPU-isa could be made semantically comparable to MiniTPU so the two machines’ cycle counts would mean the same thing. Six increments were built and independently re-measured; the answer is that the alignment is expensive enough to stop pursuing, and that result is the finding.
The variant lives on the branch tinytpu-align (10ee6882), which is
not merged into main and is kept as the record: the repros, the verification
logs, the csynth report and the per-unit table are all in that commit. The
shipped design (TinyTPU-isa: A Programmable GEMM Accelerator) is unaffected and remains the
baseline for every performance claim.
Measured, by cosim, at the five benchmark shapes: 216 / 408 / 809 / 933 /
1521 cycles with zero mismatches, against the shipped design’s 172 / 262 /
418 / 484 / 686 as it then stood — 1.26x to 2.22x the cycles. (The shipped
row has moved twice since: to 171 / 261 / 417 / 483 / 685 on 2026-09-22, and
to 175 / 265 / 421 / 482 / 674 on 2026-09-24 when QD=16 became the
default. The ratio above is against the row as it stood, and both sides of it
were measured at QD=8; re-stating it would mean re-measuring the variant
too, which has not been done. The conclusion does not turn on it — the track
was stopped by the clock, not the cycles.) At T=8 the variant measures
302 / 474 / 699, also exact. Functional gates pass throughout: bench ALL
EXACT, stress 487/487, 34 crafted bad programs rejected against 390 generated
programs accepted, and 42 mutants caught (41 by the functional gates, one only
by RTL cosim).
The cycles are not the whole cost, and this is what stopped the track.
Synthesis of the same build (xcu280, 3.33 ns target, T=4, MAXDIM=16)
estimates 3.782 ns with -1.35 ns of top-level slack — the variant does not
close at the shipped design’s frequency, where the shipped design estimates
2.431 ns with margin. The critical path is in vpu_0 and vmu_0, the two
units the MiniTPU-semantics increments created and the only two carrying
style=flp loops; every other unit still reports 2.431 ns. So the honest
comparison multiplies a 1.26-2.22x cycle cost by a roughly 1.56x longer clock,
and the variant’s resources are BRAM 41 / DSP 21 / FF 17,918 / LUT 24,244
against the shipped design’s BRAM 42 / DSP 14 / FF 17,481 / LUT 26,583.
Where the cycles went, per increment, at 4x4x4 and 16x16x16:
Increment |
Cycles |
Step |
|---|---|---|
1 ( |
176 / 750 |
+4 / +64 |
2 ( |
186 / 1216 |
+10 / +466 |
3 ( |
197 / 1904 |
+11 / +688 |
3b ( |
199 / 1514 |
+2 / -390 |
4 and 5a (branch HEAD) |
216 / 1521 |
+17 / +7 |
The two large steps are increments 2 and 3, and they are real: each was re-measured independently at its own commit with zero mismatches. Increment 3b recovered 390 cycles of increment 3’s 688.
Three claims the building agent made were corrected by the re-measurement, and
are recorded on the branch: there are 42 mutants rather than 41; frp
passes the pipeline-style repro rather than deadlocking as the register
said; and a 72-bit stream does not “run clean” — 65, 72 and 96 bits all corrupt
the simulator heap, and what varies is only whether the overrun lands somewhere
fatal (see Allo Limitations Register). Two documentation defects also stand
on that branch: DELAY is described as an honoured minimum issue spacing but
appears nowhere in the implementation, and a cited isa_dsl.eltwise_program
does not exist.
One piece of the experiment was judged worth keeping independently of it:
s.pipeline(style=) (0038833c), a ~20-line schedule primitive modelled on
rewind and covered by tests/test_vhls.py::test_pipeline_style. Any
cyclic dataflow design needs it. Note that only EmitVivadoHLS.cpp reads
pipeline_style; the Intel, Catapult, Tapa and XLS emitters ignore it
silently.
Provenance¶
The “rescued scaling report” referred to above was kept in the repository as
notes/archive/minitpu_scaling.md until the notes were moved into this
documentation. It holds the 8-stage TinyTPU scaling plan, dropped here as plan
rather than evidence. Read it with care: it describes ~/core/npu, a
different machine from the live ~/core/minitpu (see the table in the
previous section), and its scaling stages rest on the banked VMEM and on
mxu_adapter.sv, which is dead code. It is recoverable from git history:
git show a2d92cd8:notes/archive/minitpu_scaling.md
Neither source tree travels (~/core/npu is now empty), so nothing in it can
be re-derived from source on another host.
Earlier measurements and corrections¶
What this page said before, kept so the corrections are checkable. None of it is current.
The README’s stale power figure¶
The design’s README once stated 7.255 W, which was stale for the
0xB182E69D build. Fixed after we reported it; it then read 8.255 W. The
report itself is §6.
The cycle-model fit before the vmatpop correction¶
The fit this page previously carried, exact over 20 simulated points before
the vmatpop occupancy correction (72c8b78), was
46 + 194·n + 64.5·B + 303·n·B. Kept as the earlier reading, not as
current. The fixed term and the B coefficient did not move; the n and
n·B terms did.
Readings of another team’s design that this page got wrong¶
An earlier revision said the array accumulates “exactly 16 deep”, which read as an exactness guarantee. It is not one: only the first term is exact, via a zero bypass, and every later add rounds.
An earlier revision of §4 implied that figures other than the three bring-up kernels were board cycle counts. They are not.
The Verilator workload figures were stale here as well: 16×16 GEMM was published on this page as 283 cy and 16×16 GEMM→GELU as 253, against the corrected 168 and 155.
An earlier claim that
B = 1..16is bit-identical omitted the row-wise qualifier, and was wrong for attention.