Skip to content

Latest commit

 

History

31 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

KEA — an int8 NPU, its compiler, and its simulator

KEA-1 is a fully specified inference-accelerator research stack: a frozen integer ISA, functional and cycle-approximate simulators, an MLIR compiler from quantized TOSA to scheduled machine code, a runtime and artifact format, and a post-training quantization frontend.

The checked-in MobileNetV2 int8 graph lowers and executes all 183 nodes across two KEA programs. The primary bit-exact path keeps the final global-average pool on the host; the all-NPU path exposes a documented frontend rounding boundary (±1 on 11.4% of logits across four golden inputs, with 4/4 argmax agreement). Nothing here runs on silicon, and simulator cycle counts are a lower bound rather than hardware measurements.

Current scope
Machine Custom branchless ISA, systolic/depthwise/vector units, explicit DMA and semaphore synchronization
Compiler Quantized TOSA → two-level KEA MLIR → tiled, allocated, scheduled text assembly → .keaf
Execution Bit-exact functional semantics, cycle-approximate timing, runtime loader, assembler and disassembler
Evidence Native/compiler/frontend suites, golden-vector validation, scheduler A/B, and a reproducible simulator roofline

Every number in this file is generated by a checked-in command. The exact limitations and reproduction boundary appear alongside the results below.


1. The machine

KEA-1 is a statically scheduled, in-order, integer-only accelerator. It has no branches, no loops, no caches, and no hardware dependency tracking. The compiler is responsible for correctness: every cross-unit dependency is an explicit SIGNAL / WAIT on one of 32 counting semaphores.

Clock 1 GHz (nominal)
MXU 16×16 systolic array, 256 int8 MAC/cycle (512 int4), two weight banks
DWU 16 lanes × 8 MACs, 128 int8 MAC/cycle, 3×3 and 5×5 depthwise only
VPU 16 elem/cycle — requantize, quantized add, pool, strided copy
DMA0 / DMA1 two engines sharing one 16 B/cycle DRAM port
Scratchpads SPM_A 256 KiB, SPM_W 256 KiB, ACC 32768 × int32 (word-addressed)
IMEM 1 MiB = 32768 instructions, fixed 32 B each. There is no loop instruction — a network is fully unrolled
Dispatch one instruction per cycle, program order, into five depth-16 in-order queues

Two consequences shape everything above it. First, a full queue stalls the dispatcher, so a badly ordered instruction stream serialises the whole machine. Second, IMEM is a hard capacity limit on network size, not a performance knob — see §4.

Peak arithmetic: 512 GOPS int8 (1 MAC = 2 ops). Peak bandwidth: 16 GB/s. The ridge point is therefore 32 ops/byte — a layer below that is memory bound no matter how well it is scheduled.

docs/ISA.md is normative; docs/MICROARCH.md freezes the timing model.


2. The stack

model.py ──frontend──▶ model.kgraph.json + .npz
                           │  kea_frontend.tosa_emit
                           ▼
                     model.tosa.mlir
                           │
                        kea-opt   (tosa/linalg ─▶ kea ─▶ fuse ─▶ tile ─▶ alloc ─▶ schedule)
                           │
                        kea-translate
                           │
             ┌─────────────┼─────────────┐
             ▼             ▼             ▼
       model.kasm    model.weights.bin  model.map.json
        (assembly)     (constants)     (DRAM map, I/O
             │             │            tensors, metadata)
             └─────────────┼─────────────┘
                           ▼
                        kea-as
                           ▼
                       model.keaf  ──▶ kea-rt / kea-sim

(from ADR-0001; keac is a thin driver that runs the whole chain.)

The text-assembly boundary is the load-bearing design decision. The MLIR half never writes binary and never links isa.h; the native half never links MLIR. The payoff is that the scheduled, allocated, double-buffered instruction stream is readable. §6 shows what that buys.

The kea dialect is deliberately two-level (ADR-0002): Level 1 is value-semantic tensors with quantization as structured attributes; Level 2 is buffers, addresses, events and machine ops. The ingest format is TOSA, not linalg, precisely so the quantization survives exactly (docs/DIALECT_L1.md §6).


3. Build and run

Toolchain: LLVM/MLIR 20.1.6 only (docs/TOSA_NOTES.md §16 lists what breaks on 21+), a C++17 compiler, and the venv described in docs/PLATFORM.md.

source scripts/env.sh
bash scripts/build_compiler.sh     # kea-opt, kea-translate + 35 lit tests (~6 min)
bash scripts/build.sh              # kea-as, kea-dis, kea-sim, kea-rt, keac + 17 tests
.venv/bin/python -m pytest frontend/tests        # 92 tests
.venv/bin/python tools/keac/tests/numeric_check.py   # 7 layouts, bit-exact

The end-to-end demo — emit TOSA from the quantized graph, compile, simulate, validate against golden vectors, and produce the roofline:

bash demo/run_all.sh          # ~2 min;  --quick skips the scheduler A/B

Compiling one model by hand — no flags are needed; the defaults fit:

# .kgraph.json -> TOSA MLIR
cd frontend && ../.venv/bin/python -m kea_frontend.tosa_emit \
    ../models/mobilenetv2_int8.kgraph.json -o /tmp/m.tosa.mlir \
    --function mnv2 --last-index 178

# TOSA -> .keaf, then run it
build/native/bin/keac /tmp/m.tosa.mlir --function mnv2 -o /tmp/m.keaf \
    --schedule --keep-intermediates
build/native/bin/kea-sim /tmp/m.keaf --stats-json /tmp/m.stats.json

keac --imem-budget <n> exposes the tiler's whole-function instruction budget — the knob that picks the tiling, and therefore the cycle count. The default is the measured optimum for this network; docs/RESULTS.md §6.2 has the curve, including the cliff below it.


4. Headline numbers, and the caveats that go with them

MobileNetV2 int8, 224×224, batch 1, models/mobilenetv2_int8.kgraph.json (torchvision IMAGENET1K_V1, percentile observer, BN folded). Everything at the compiler's defaults — the demo passes no tuning flag anywhere.

Nodes compiled and run on the NPU 183 of 183 — nothing runs on the host
Convolutions 52 of 52, all scheduled
Programs 2 — the network is 37,295 instructions at its coarsest tiling and IMEM holds 32,768
Total cycles 3,230,350 = 3.23 ms at 1 GHz (3,854,857 unscheduled)
Instructions 40,099 across the two programs
Useful arithmetic 601.6 Mops (300.8 M MACs)
DRAM moved 22.7 MB (16.0 MB read, 6.7 MB written)
Arithmetic intensity 26.5 ops/byte — below the 32.0 ridge point, so memory bound
Achieved 186.2 GOPS of 424.7 attainable = 43.9%
MXU MAC utilisation 33.9% of the 256 int8 MAC/cycle peak
MXU padding efficiency 93.3% (useful ÷ issued MACs)
Numerical result bit-exact on the split that keeps the pool on the host; ±1 on 11.4% of logits, argmax 4/4, with the pool on the NPU
--kea-schedule A/B 1.181× on the 52-convolution feature extractor, 1.193× on the whole network

Caveats, in order of how much they matter.

  1. MobileNetV2 does not fit in one program, and cannot. At the coarsest tiling of every layer it is 37,295 KEA-1 instructions against a 32,768 entry IMEM — 13.8% over. That is a capacity result, not a bug: the machine is branchless and IMEM is not paged, so program size is a hard limit. Every op in the graph lowers; it is purely size. docs/RESULTS.md §2.1 measures it three ways.
  2. All 183 nodes on the NPU, or bit-exactness — not both. The cut can go either side of the head pool. Keep the pool on the host and the result is bit-exact against the numpy reference on all four golden vectors. Put it on the NPU and 11.4% of the logits shift by exactly ±1, with the argmax unchanged 4/4 — because the frontend can only spell a scale-changing pool as avg_pool2d + rescale, which rounds twice where the reference rounds once. That is a Level 1 dialect limitation, not a backend one. docs/RESULTS.md §4.2.
  3. kea-sim cycle counts are a lower bound. Scratchpad port arbitration, DRAM row buffers, refresh and turnaround, and misalignment costs are all unmodelled (docs/SIMULATOR.md §2). Real LPDDR would cost 10–30% more on scattered access — and the classifier, at 94.8% of attainable and 15.2 GB/s of a 16 GB/s port, is where that would show first.
  4. The imem-budget window is narrow and has a cliff. Only ~19,100–21,000 is feasible at all. Below 20,000 the tiler makes too few, too large tiles for the scheduler to overlap, and --schedule correctly declines — emitting output byte-identical to not scheduling. The default sits at the measured optimum. docs/RESULTS.md §6.2.
  5. Accuracy figures for the quantized graph itself (67.9% top-1 on a 1000-image Imagenette split, not ImageNet-1k) live in docs/FRONTEND.md §8 with their own caveats. This repository's contribution is that the compiled artifact reproduces the reference exactly, not that the reference is accurate.

4.1 Bugs found and fixed during bring-up

Compiling a real network found six backend defects; all six are fixed, and bash demo/regress/run_regressions.sh asserts the fixed behaviour — 11 assertions — so they cannot silently come back.

was now
kea.pool name collision every pool untranslatable — a DRAM name and an SPM_A tile name collided uniquification moved into makeBuffer(); a 7×7 pool runs in 460 cycles at 42.6% VPU
activation-RHS kea.matmul error: null operand found, pointing at nothing a real diagnostic naming the op and the weight-stationary reason (the limitation itself stands)
Rule D violation --kea-schedule failed on any prefix past node 97 the whole 52-convolution extractor schedules, and validates bit-exactly
IMEM overrun 41,409 instructions at the default reserve factor; the demo was pinned to --spm-reserve 1 -kea-tile is IMEM-aware; the extractor fits at the defaults with no flags
standalone kea.rescale no Level 2 lowering, so a scale-changing pool could not compile at all lowered through a 16×16 identity matmul — this is what puts the pool on the NPU
kea.trace marker hoisting 10 of 52 TRACE regions opened at cycle ~0; per-layer numbers in a scheduled build were nonsense (Σ regions = 7.43× the program) markers key on their own queue, bounded by a region lookahead; Σ regions = 1.072×, checked on every build by demo/common.py's own detector

Half of them were diagnosis or instrumentation bugs rather than capability bugs — the compiler could already do more than it appeared to, and small reproducers were what showed that. Details in docs/RESULTS.md §3.

5. What the roofline says

KEA-1 MobileNetV2 simulator roofline

Cycle-approximate simulator results at the compiler defaults; these are not silicon measurements. The plot is regenerated by bash demo/run_all.sh.

The two layer families separate cleanly, which is the whole point of plotting it:

arithmetic intensity achieved share of ops share of layer-cycles share of DRAM
conv (MXU), 35 layers 16.5 – 92.0 ops/B 118 – 316 GOPS 90.7% 74.5% 62.3%
3×3 depthwise (DWU), 17 layers 3.6 – 10.0 ops/B 38 – 101 GOPS 8.9% 23.0% 32.3%
classifier fully_connected 1.9 ops/B 29.2 GOPS 0.4% 2.5% 5.3%

All 17 depthwise layers, the classifier and 13 of the convolutions fall below the ridge point: 31 of 53 layers are memory bound. A depthwise 3×3 does 9 MACs per input element regardless of channel count, so its intensity is fixed by the kernel, not by the layer size; the classifier reads 1.28 MB of weights to do 1.28 M MACs and can never be anything but bandwidth limited. No amount of scheduling moves those points up; only fusing them into their neighbours (so the activation never round-trips through DRAM) would.

Two layers are worth naming. The last 320→1280 pointwise at 7×7 is the fastest in the network — 92.0 ops/byte, 316.4 GOPS. And the classifier, the least efficient layer by MAC utilisation (5.7%), is the most efficient by the only measure that applies to it: 94.8% of attainable, 15.2 GB/s of a 16 GB/s port. It is the one layer here that is genuinely finished.

Layer points and both network points are the scheduled build; the two stars are the whole network before and after --kea-schedule.

6. Why text assembly: a DMA visibly overlapping a MATMUL

This is demo/results/schedule_excerpt.kasm lines 46–65, the scheduled stream for one 112×112 pointwise convolution. Long operand lists are wrapped and a few stride fields elided as ; nothing is reordered. Four things are happening at once: DMA1 is loading the next row band between the two MATMULs of the current one, the VPU is requantizing the previous band's accumulator, DMA0 is storing the band before that, and LOAD_W alternates bank=0 / bank=1 so a weight load never waits for the array:

  MXU   WAIT    event=3, threshold=1
  MXU   WAIT    event=4, threshold=1
  MXU   LOAD_W  w_addr=w:0, w_row_stride=16, k_rows=16, n_cols=16, bank=0, dtype=int8
  MXU   MATMUL  a_addr=a:28688, a_inner_stride=32, a_outer_stride=3584, m_inner=112,
                m_outer=8, acc_addr=acc:14336, …, bank=0, acc_mode=overwrite, dtype=int8
  DMA1  WAIT    event=2, threshold=1
  DMA1  DMA_LD  spm_space=SPM_A, dram_addr=@slice_6_7.input0+86016,               <- next band in,
                spm_addr=a:86064, len0=3584, n1=8, n2=1, dram_s1=3584, …            mid-MATMUL
  DMA1  SIGNAL  event=4, inc=1
  VPU   WAIT    event=5, threshold=1
  VPU   WAIT    event=6, threshold=1
  VPU   VQUANT  acc_addr=acc:0, out_addr=a:172128, qparam_addr=w:512,             <- previous band's
                num_pixels=896, channels=16, …, out_zp=-5, dtype=int8                accumulator
  VPU   SIGNAL  event=3, inc=1
  VPU   SIGNAL  event=1, inc=1
  MXU   LOAD_W  w_addr=w:256, w_row_stride=16, k_rows=16, n_cols=16, bank=1, dtype=int8
  MXU   MATMUL  a_addr=a:28704, a_inner_stride=32, a_outer_stride=3584, m_inner=112,
                m_outer=8, acc_addr=acc:14336, …, bank=1, acc_mode=accumulate, dtype=int8
  MXU   SIGNAL  event=5, inc=1
  MXU   SIGNAL  event=7, inc=1
  DMA0  WAIT    event=7, threshold=1
  DMA0  WAIT    event=1, threshold=1
  DMA0  DMA_ST  spm_space=SPM_A, dram_addr=@slice_6_7.0.out, spm_addr=a:172128,   <- band before that
                len0=16, n1=112, n2=8, dram_s1=16, dram_s2=1792, …                   out

The unscheduled build of the same layer (demo/results/schedule_excerpt.unscheduled.kasm) issues every DMA_LD on DMA0, never touches DMA1, and puts a WAIT between every producer and consumer. It takes 102,764 cycles; the version above takes 53,113 — 1.935×. Over the whole feature extractor the same mechanism is worth 1.181×, and kea-sim says the answer is still bit-identical.

You cannot review that property in a binary, and you cannot unit-test it without a parser. That is the argument for the boundary.


7. Repository layout

include/kea/          the frozen ISA: hw_config.h, isa.h, program.h, keaf.h
compiler/             out-of-tree MLIR: the `kea` dialect, conversions,
  include/kea/        fuse/tile/schedule/alloc passes, the .kasm emitter
  lib/                    Dialect/ Conversion/ Transforms/ Target/Kasm
  tools/              kea-opt, kea-translate
  test/               lit tests (35)
sim/                  the cycle-approximate simulator (functional + timing)
runtime/              KEAF reader/writer, the .kasm assembler, kea-rt
tools/                kea-as, kea-dis, kea-sim, keac  (+ keac's e2e tests)
frontend/             PTQ: torch/ONNX -> .kgraph.json, the numpy golden model,
  kea_frontend/         and tosa_emit.py -- .kgraph.json -> TOSA MLIR
models/               the shipped quantized graphs (MobileNetV2, a tiny ViT)
tests/mlir/           verified TOSA and linalg examples for 20.1.6
tests/invariants/     cross-component invariants (e.g. requant equivalence)
demo/                 the end-to-end demo, its results, and regress/ --
                        regression tests for the defects found in bring-up
docs/                 the specifications; adr/ for the three decisions

8. Where to read next

If you want Read
the end-to-end result, per layer, with every caveat docs/RESULTS.md
the instruction set docs/ISA.md, then docs/ISA_ERRATA.md
the timing model the simulator implements docs/MICROARCH.md, docs/SIMULATOR.md
what TOSA actually looks like on 20.1.6 docs/TOSA_NOTES.md — machine-verified, trust it over upstream docs
the kea dialect docs/DIALECT_L1.md (tensors), docs/DIALECT_L2.md (machine)
how conv becomes MATMULs docs/CODEGEN.md
tiling, buffers, DRAM layout docs/MEMORY_PLANNING.md
the list scheduler and its cost model docs/SCHEDULING.md
requantization, bit-exactly docs/QUANTIZATION.md + docs/adr/0003-*
quantization, calibration, accuracy docs/FRONTEND.md
the .kasm and .keaf formats docs/ASSEMBLY.md, docs/ARTIFACT_FORMAT.md
why the pipeline is shaped this way docs/adr/

About

End-to-end int8 inference accelerator research stack with a custom ISA, cycle-approximate simulator, MLIR compiler, PTQ frontend, and reproducible model execution.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages