← projects

Mirage — An Agentic Chip Design Case Study

Mirage is an open-source Transformer accelerator for real-time EN↔AR translation on one of the smallest Zynq parts sold — a machine-translation engine that has to fit in 66 DSP slices. This page is about the agent loop that carries its design program: it audits the upstream RTL, drafts what's missing, and closed 19 of 19 plan tasks through a gate that only believes files on disk.

Mirage top-level block diagram, four quadrants: a 12×5 DSP matrix-multiplication array; multi-head attention with Q/K/V weight BRAMs and a finite state machine; the softmax path with max finder, Taylor approximation and quantizers; and the feed-forward network with projection, hidden and output layers behind BRAM caches.
Fig. 0 — Upstream Mirage top-level block diagram (Mirage/images/mirage-top-level-bd.png): the four cores the plan's audits and drafts land in.

01 The problem

The silicon constraint: a 6-layer, 256-dim encoder has to run on an XC7Z007S — 66 DSP48E1 slices, ≈225 KB of BRAM — so one physical encoder layer must stand in for all six. Pre-synthesis, the multiplier budget looks like this:

DSP48E1 floorplan of the XC7Z007S: 65 of 66 slices allocated A grid of 66 cells, one per DSP48E1 slice. 60 cells are allocated to the gemm_array matrix-multiply tile, 4 to the taylor_exp exponential unit, 1 to the agent-drafted softmax modules, and exactly 1 cell is free. Pre-synthesis estimate from reports/w11-resource-map.md. XC7Z007S · DSP48E1 BUDGET · PRE-SYNTHESIS ESTIMATE (W11)
Fig. 1 — 65 of 66 DSP48E1 slices allocated (reports/w11-resource-map.md). The W11 task's <90% acceptance bar expects this to fail; the documented fallback time-multiplexes the gemm tile as 2×30. One free multiplier is the entire margin for error.

The other constraint is trust: a chip is a long chain of decisions where a week-3 arithmetic error quietly poisons week 9. So every task is gated on evidence, and every claim below carries its evidence tier — nothing here ran on silicon.

02 Architecture

Two architectures share this project: the harness that does the engineering, and the datapath being engineered. The agent occupies exactly one box.

The harness

Harness architecture: plan, gate CLI, agent, and evidence tools Four zones left to right. PLAN: the spec, the 19-task plan, and the state file. GATE: the workflow CLI whose complete command exits 2 unless declared artifacts exist on disk. MODEL: the Kimi K3 agent with pre-tool scope guards beneath it. EVIDENCE: ghdl, Python golden models, report artifacts, and the trace feed to a live dashboard. Task briefs of at most 500 tokens flow from the state file through the CLI to the agent; the agent drives the tools; the gate re-reads the disk before closing a task and writes results back to the state file. PLAN GATE · workflow.cli MODEL EDA · EVIDENCE mirage-mvp-spec mission + exit bar 19-task plan parse.py · stable IDs mirage.state.json 59 nodes · truth workflow.cli replies ≤500 tokens artifact gate exit 2 on missing files Kimi K3 agent plans · writes · repairs scope guards Mirage/ is read-only ghdl -a · -e · -r goldens spec-first · py artifacts reports/ · outputs/ trace → dashboard :8787 parse brief ≤500 tok one task gate re-reads the disk veto log verdict

◂ swipe to pan the diagram ▸

harness Select any block — the artifact gate is the one that matters.

data · artifacts control · gate checks purple = the agent dashed outline = agent-drafted

Fig. 2 — The harness (workflow/, harness/hooks.py). Briefs come from the state file, evidence from ghdl and spec-first goldens; completion is decided by a gate that reads the disk, not the transcript.
All node notes (text version)
spec/mirage-mvp-spec.md
The mission and exit bar, written before any RTL was touched — the agent is held to a standard it did not choose.
19-task plan
Parsed from the spec by workflow/parse.py into stable task IDs, each with acceptance criteria, declared artifacts, and a risk note.
spec/mirage.state.json
59 nodes; the single source of truth for progress. Every number on this page traces to this file or a report it points at.
workflow.cli
status · next · show · start · complete · block; every reply capped at ~500 tokens.
artifact gate
Refuses with exit 2 unless every declared artifact exists on disk; --force is human-only and was never used.
Kimi K3 agent
Plans, writes Python golden models, drafts and repairs VHDL; finds out whether it succeeded from exit codes.
scope guards
Pre-tool hooks block writes into the read-only Mirage submodule, plus rm -rf, git push, curl-pipe-bash.
ghdl
Analyze, elaborate, run — catches VHDL that will not build; surfaced the week-7 FFN compile blocker.
goldens
Bit-accurate Python references written from the spec before the RTL is read.
artifacts
The declared files each task must leave behind — what the gate checks and what this page cites.
trace → dashboard
workflow/kimi_bridge.py tails the agent's wire log into trace events for a local live dashboard.

The datapath

One physical encoder layer, walked six times per sentence; int8 weights stream from DDR while int16 activations stay in fabric. Four blocks carry real defects the loop found.

Mirage datapath: processing system, AXI stream, and the encoder layer instantiated once Top zone: the Zynq processing system with the ARM Cortex-A9, AXI DMA and DDR3, feeding an AXI4-Stream rail. Bottom zone: the programmable logic holding token embedding, weight stream controller, score BRAM, and the encoder layer blocks — qkv score calculation, taylor exponential, softmax core, two add-and-norm stages, feed-forward network, and output projection. A return edge marks the same physical layer being reused six times. Dashed outlines mark agent-drafted modules. PROCESSING SYSTEM · ZYNQ XC7Z007S ARM Cortex-A9 PS · driver + tiling AXI DMA · HP0 weights + activations DDR3 all 6 layers of weights UART console demo host · not yet run AXI4-STREAM · 100 MB/s planning assumption PROGRAMMABLE LOGIC · 14 400 LUT · 66 DSP48E1 · 50 RAMB36 ENCODER LAYER · INSTANTIATED ONCE token embedding int8 · 256-dim weight stream ctrl prefetch layer n+1 score BRAM 128 × 21 bit qkv_score_calc Q·Kᵀ · 8 heads taylor_exp_approx Q9 · |x| ≤ 0.5352 smx_core softmax · V add & norm ① int16 inter-stage ffn_core 256 → 1024 → 256 add & norm ② int16 inter-stage output proj → logits logits ×6 · time-multiplexed layer reuse

◂ swipe to pan the diagram ▸

datapath Select any block for the audit note recorded against it.

data flow control · prefetch heavy edge = layer reuse ×6 dashed outline = agent-drafted

Fig. 3 — Datapath from spec/mirage.state.json and the W6–W11 reports; reusing one layer six times is what makes it fit. Target: Digilent Cora Z7-07S — 14,400 LUT · 66 DSP48E1 · 50 RAMB36, 512 MB DDR3, one 667 MHz Cortex-A9 (Digilent reference manual); budget per reports/w11-resource-map.md.
All node notes (text version)
ARM Cortex-A9 · PS
Builds DMA descriptors and drives the tiling loop; runs the C driver, which has never run on a board.
AXI DMA · HP0
Streams int8 weights and activations. Post-optimization the pipeline is DDR-bound; crossover ≈192 MB/s vs the 100 MB/s planning assumption.
DDR3
All six layers of weights; ≈278 KB int8 per layer vs ≈1.11 MB fp32 — a 4× reduction.
UART console
Demo host; nothing has executed on silicon.
token embedding
int8, 256-dim, streamed from DDR.
weight_stream_ctrl (drafted)
Prefetches layer n+1 during compute — the W15 optimization: 25.98 → 16.74 ms.
score BRAM · smx_cache (drafted)
128 × 21-bit entries; upstream file never existed; modelled drift 0 LSB.
qkv_score_calc
W1 audit: 21-bit score_sum overflows by exactly 1 at 2²⁰; testbench not self-checking.
taylor_exp_approx
Met its <1% bar only at x = 0; Q9 fix reaches 0.9683% max error.
smx_core (drafted)
As found 87–107 LSB from golden (scaling off ≈2¹⁸); fixed to worst 2.99 LSB, RMS 0.52.
add & norm ①/②
int8 inter-stage drifts past 1 LN unit by layer 1; int16 holds 0.0062 LN worst over 3 layers.
ffn_core
Failed 10/10 trials, did not compile, cannot fit its weights on-chip; closed with a redesign spec.
output projection
Vocabulary logits back to the PS; decode covered by golden models.

03 The loop

"Verify the softmax" is not a task — it cannot fail. "smx_core matches the Python golden for 8 heads × 1 layer × 4 tokens" is a task. The loop runs five verbs against 19 of those, one task at a time:

The agent loop as a cycle, with the decision that terminates it Five nodes in a cycle: next, start, execute, verify, then a decision diamond asking whether the declared artifacts exist on disk. Yes leads to task closed — 19 of 19, none forced — and back to next. No leads back to execute for at most two retries; a third failure leads to blocked, which only a human --force can clear. next state file picks the task start locks exactly one open execute golden model → RTL verify run the numeric check artifacts on disk? task closed 19 / 19 · none forced blocked human --force only yes next task · ×19 no · retry ≤2 3rd failure

◂ swipe to pan the diagram ▸

Fig. 4 — The only path from "I think I'm done" to closed runs through the gate; the only way past a block is a human. Contract: harness/loop.py, workflow/query.py.
the gate, refusing
# close a task whose declared report does not exist yet
$ python -m workflow.cli complete p1/w3 --artifacts reports/w3-exp-error-histogram.md
REFUSED p1/w3: acceptance not verified
  artifact missing: reports/w3-exp-error-histogram.md
  fix, or re-run with --force to override
$ echo $?
2

Format as implemented in workflow/cli.py; --force was used zero times.

One iteration, end to end

Week 3, the exponential unit — from the run's own files. Stepping through lights the active path in the cycle above.

  1. the harness picks the task

    $ python -m workflow.cli next id: p1/w3 title: taylor_exp_approx acceptance: - Polynomial exp core within <1% relative error for |x| <= chosen clamp vs high-precision reference (error histogram produced).

    The state file chooses the task, never the agent's memory; replies cap at ~500 tokens.

  2. golden model before RTL

    outputs/taylor_exp_model.py — bit-accurate reference, written from the spec before the RTL is read

    Spec-first: the reference cannot inherit the RTL's mistakes.

  3. the audit fails the RTL

    As-coded RTL FAILS (clamp 0; 64/64 points ≥100% err)

    It met the bar at exactly one input, x = 0 — an implicit clamp swallowed the input (spec/mirage.state.json).

  4. the documented fix

    Q9 fix (S=512, clamp |x| ≤ 0.5352): max rel err 0.9683% — PASS

    Root cause: Q-format. Two retries allowed; a third failure blocks the task (reports/w3-exp-error-histogram.md).

  5. the gate reads the disk

    $ python -m workflow.cli complete p1/w3 \ --artifacts outputs/taylor_exp_model.py reports/w3-exp-error-histogram.md p1/w3: done (verified=True)

    All 19 tasks closed this way; --force was never used.

Ungated agent flow versus this project's gated flow Two columns with identical shapes. Left, without a gate: task, agent, a dashed node labelled looks done whose evidence is the transcript, then next task — the claim is trusted. Right, with the gate: a task brief of at most 500 tokens, the agent, artifacts on disk, then a gate that re-reads the disk and either closes the task with exit 0 or refuses with exit 2. WITHOUT A GATE WITH THE GATE · THIS PROJECT task agent "looks done"evidence: the transcript next task trusted task brief≤500 tokens agent artifacts on diskreports/ · outputs/ gate re-reads the diskexit 0 closed · exit 2 refused checked

◂ swipe to pan ▸

Fig. 5 — Same shapes, one difference: what counts as evidence. Everything else on this page follows from that swap.

The ledger — all 19 closures

From spec/mirage.state.json — the bar each task was held to and the artifact it traces to.

P1 · Attention core — W1–W6

W1 qkv_score_calc finalization
bar ≥95% element-wise agreement (±1 LSB) vs Python across 3 seeds.outcome Audit found a 21-bit score_sum overflow-by-1 edge case at 2²⁰, and a testbench that was not self-checking.artifact reports/w1-qkv-verification.md
W2 score_bram completion
bar Integrated multi-token sim with deterministic cycle counts.outcome Sizing verified (128 × 21 bit); latency model ≈900 cycles per 4-token batch, deterministic by construction.artifact reports/w2-latency.md
W3 taylor_exp_approx
bar <1% relative error over the clamp range, histogram produced.outcome As-coded RTL fails 64/64 sampled points; documented Q9 fix reaches 0.9683% max error.artifact reports/w3-exp-error-histogram.md
W4 smx_cache
bar ±1 LSB partial-sum drift on an 8-head × 16-token test.outcome Upstream file never existed — agent drafted it; modelled drift over 128 rows: 0 LSB.artifact outputs/smx_cache.vhd · reports/w4-smx-cache.md
W5 smx_v_calc + smx_core
bar End-to-end attention vs Python, 8 heads × 1 layer × 4 tokens.outcome As found: 87–107 LSB off (scaling ≈2¹⁸). With fix: worst 2.99 int8 LSB, RMS 0.52.artifact outputs/smx_core.vhd · reports/w5-smx-e2e.md
W6 mha_core integration
bar 1 sentence (≥6 tokens), all heads, vs software.outcome PASS (worst 3.98 LSB, RMS 0.59); resource profile: gemm ≈60 DSP, ≈1.5 ms/layer at 200 MHz.artifact reports/w6-resource-latency.md

P2 · FFN & encoder — W7–W10

W7 FFN redesign at full dims
bar Standalone FFN passes 10 random trials vs software.outcome Upstream FFN fails 10/10 at 100% error, does not compile, cannot fit its weights. Redesign specified: 16-bit activations, power-of-two scales.artifact reports/w7-ffn-verification.md
W8 Encoder layer integration
bar Single layer vs software at 10–12 tokens.outcome PASS at 12 tok × 8 heads: worst 0.0296 LN units, RMS 0.0095; smx drafts ghdl-clean.artifact reports/w8-encoder-layer.md
W9 Multi-layer scheduling
bar 2–3 sequential layers verified vs golden.outcome 3-layer chain passes with int16 inter-stage (worst 0.0062 LN); int8 fails by layer 1. Weight traffic 278,528 B/layer.artifact reports/w9-multilayer.md
W10 Performance report v1
bar Cycles/token, tok/s at target clock, drift check vs baseline.outcome ≈51.4k cycles/token/layer; 6-layer ≈360 tok/s overlapped → ~24× headroom vs ~15 tok/s real-time NMT.artifact reports/perf-report-v1.md

P3 · Platform bring-up — W11–W12

W11 Synthesis and initial P&R
bar Post-synth timing + resource map, <90% of critical resources.outcome Vivado is human-gated: skeletons + pre-synth estimates instead. DSP 65/66 (98%) — expected fail, 2×30 mitigation documented.artifact reports/w11-resource-map.md
W12 Timing closure and bitstream v1
bar Bitstream builds; driver init succeeds if board available.outcome C driver with real ISR + poll fallback written; mirage_v1.bit explicitly not fabricated — pending a human Vivado run.artifact driver/mirage_driver.c · reports/w12-bitstream-timing-closure.md

P4 · End-to-end translation — W13–W14

W13 EN→AR greedy demo
bar Greedy decode end-to-end with correctness samples vs software.outcome 5/5 episodes token-identical vs float golden (toy weights — trained checkpoint absent).artifact reports/demo-v0.md
W14 AR→EN bidirectional demo
bar Both directions run, initial perf table produced.outcome 10/10 token-identical. First real ghdl sims: gemm 36/36 dot products, start→done 1 clk (2 clk/pass effective).artifact reports/perf-table-bidirectional.md

P5 · Optimization & beam — W15–W16

W15 Optimization pass
bar Before/after delta ≥10% improvement.outcome −36% sentence latency (25.98 → 16.74 ms), +56% throughput; pipeline now DDR-bound (crossover ≈192 MB/s).artifact reports/optimization-report-v1.md
W16 Beam search + evaluation draft
bar Comparative metrics vs CPU baseline; token accuracy on samples.outcome Beam 2/4: 10/10 token-identical; beam 1 flips 2 near-ties (13/15). CPU 5.43 s vs cycle-model 3.18 ms per sentence.artifact reports/evaluation-report-draft.md

P6 · Hardening & release — W17–W19

W17 Hardening and robustness
bar RC1 tag; fault-injection matrix green.outcome ALL GREEN — single-bit upsets absorb at ≤0.051 LN (limit 0.5). Real finding: FFN accumulator can overflow int32 by 4.0× at adversarial all-max → RC2 hardening item.artifact reports/robustness-matrix.md
W18 Final polish and pitch deck
bar RC2 + pitch deck; regression re-runs with zero deltas.outcome RC2 hardening ghdl-clean; regression 10/10 green; pitch appendix separates measured / model-level / estimated claims.artifact reports/pitch-deck-technical-appendix.md
W19 MVP freeze
bar v1.0 with independent golden re-run + checksum archive.outcome Frozen: re-run 10/10 green, checksums 49/49 OK. Remaining steps are human-only: board run, checkpoint fetch, LICENSE.artifact outputs/mirage_mvp_v1.0.md · reports/final-regression.log

04 Results

What the audit found

Golden model first, RTL second kept finding real upstream defects — the project's strongest evidence:

Defects found in upstream RTL (reports W1–W9)
ModuleAs foundRoot causeAfter the loop
taylor_exp64/64 sampled points ≥100% error; bar met only at x = 0implicit clamp swallowed the input (Q-format)0.9683% max error (Q9, |x| ≤ 0.5352)
smx_core e2eattention 87–107 int8 LSB from goldenexp input scaling off by ≈2¹⁸worst 2.99 LSB, RMS 0.52
ffn_core10/10 trials fail; won't compile; weights don't fit the deviceno declaration for "mac_array"; 278 KB vs 225 KB BRAMredesign spec — 16-bit activations, streamed weights
6-layer stackdrift >1 LN unit by layer 1softmax chaotic under int8 inter-stage quantizationint16 inter-stage: 0.0062 LN worst over 3 layers

The release gate is one command; the recorded v1.0 re-run, verbatim:

reports/final-regression.log
$ bash outputs/run_regression.sh
== model-level golden checks ==
PASS  taylor_exp_model.py
PASS  smx_e2e_model.py (2.99 < 4.0)
PASS  ffn_model.py
PASS  encoder_e2e_model.py (0.0296 < 0.05)
PASS  multilayer_e2e_model.py (0.0047 < 0.01)
PASS  greedy_decode_model.py
PASS  bidirectional_demo.py
PASS  beam_search_model.py (13 >= 13)
PASS  robustness_model.py
== RTL (ghdl) checks ==
PASS  ghdl:gemm_latency_tb
==
REGRESSION: ALL GREEN
exit=0

The numbers

19/19

tasks closed through the artifact gate, zero --force

state file

4

real defects found in the upstream RTL

reports W1–W9

2.99LSB

worst end-to-end attention error after fixes

golden model

16.74ms

6-layer sentence latency, overlapped streaming

cycle model @ 200 MHz

Optimization pass: sentence latency before and after Two horizontal bars. W10 baseline: 25.98 milliseconds per 6-token sentence. W15 optimized: 16.74 milliseconds, a 36 percent reduction, with throughput up 56 percent from about 231 to about 360 tokens per second. Cycle-model figures at 200 megahertz. OPTIMIZATION PASS · SENTENCE LATENCY · CYCLE MODEL W10 baseline 25.98 ms W15 optimized 16.74 ms · −36% throughput ≈231 → ≈360 tok/s (+56%) · reports/optimization-report-v1.md
Fig. 6 — The W15 pass overlapped weight streaming with compute against a ≥10% bar and delivered −36%. The pipeline is now DDR-bound; the crossover back to compute-bound is ≈192 MB/s.
Inter-layer drift: int8 versus int16 inter-stage activations Bar chart with a dashed budget line at 0.01 LayerNorm units. With int8 inter-stage activations, drift exceeds 1 LN unit by layer 1 — about 100 times the budget, drawn off scale. With int16, layer 0 drifts 0.0043 and layer 2 drifts 0.0062, both inside budget. Model-level results from reports/w9-multilayer.md. DRIFT ACROSS STACKED LAYERS · LN UNITS · MODEL-LEVEL budget 0.01 int8 · layer 1 >1 LN — off scale, ≈100× budget → FAIL int16 · layer 0 0.0043 int16 · layer 2 0.0062
Fig. 7 — Why int16 between stages: softmax is chaotic under coarse quantization, so int8 inter-stage error compounds. Weights stay int8 — precision spent only where drift lives (reports/w9-multilayer.md).
Resource utilization against the device, pre-synthesis Three horizontal bars. DSP48E1: 65 of 66, 98 percent, past the task's 90 percent bar — flagged red. RAMB36: 42 of 50, 84 percent. LUT: about 3,600 of 14,400, 25 percent. Pre-synthesis estimates from reports/w11-resource-map.md; no Vivado run exists. RESOURCE MAP · PRE-SYNTHESIS ESTIMATE · NO VIVADO RUN DSP48E1 <90% bar 65 / 66 · 98% RAMB36 42 / 50 · 84% LUT ≈3.6k / 14.4k · 25%
Fig. 8 — DSP at 98% is expected to fail the task's <90% bar; the documented mitigation time-multiplexes the gemm as 2×30, trading latency the 24× headroom can afford. BRAM holds only because FFN weights stream from DDR.
CPU reference versus FPGA cycle model, log scale Two bars on a logarithmic scale for one 8-step toy-scale sentence: the measured pure-Python reference takes about 5.43 seconds; the FPGA cycle model estimates 3.18 milliseconds — a ratio of about 1,700. The caption carries the caveat that an optimized int8 CPU runtime would narrow this to roughly 10 to 20 times. IDENTICAL TOY-SCALE COMPUTE · PER SENTENCE · LOG SCALE Python (measured) ≈5.43 s FPGA (cycle model) ≈3.18 ms · ≈1,700×
Fig. 9 — 1,700× is against unoptimized Python; the project's own evaluation expects an optimized int8 CPU runtime to narrow it to roughly 10–20× (reports/evaluation-report-draft.md §2).

Evidence tiers & the spec's exit bar

ghdl-simulated

gemm tile 36/36 dot products, start→done measured at 1 clk. ffn_tb exits 0. All agent-drafted VHDL analyzes and elaborates clean.

Model-level golden

All accuracy claims — LSB drift, LN error, token-identical decode — come from bit-accurate Python models vs float references, not full-pipeline RTL sim.

Estimate / projection

Latency, throughput, the ≈1,700× comparison and the resource map are cycle-model or pre-synthesis figures. No board, no bitstream, nothing measured on silicon.

Spec exit criteria — where each actually stands
Exit criterionStandingStatus
EN→AR & AR→EN encoder translation10/10 token-identical vs golden — toy random weights; no trained checkpoint, no BLEUmodel-level
≥3× latency vs CPU baselinecycle model clears it with margin; definitive int8-CPU comparison needs the checkpointmodeled
<90% LUT & <95% BRAM, timing metLUT 25%, BRAM 84% pre-synth; timing unmeasured; DSP 98% vs the task's <90% barpre-synth
Open-source repo + build/sim docsregression script and per-week reports exist; LICENSE missing — owner decision, release blockerblocked
Demo script + pitch deckboth exist; demo video and board run are human stepsdone

The loop held. Nineteen tasks closed, none forced, and the four upstream defects it found are the class of bug a skimming human misses and a golden model does not. What needed a board, a license, or a trained checkpoint it did not claim — the gate never let it. Next: Vivado synthesis and board bring-up, replacing every estimate above with a measurement.

05 Stack & my role

WhoWhat
Built by me The workflow engine (parse.py · model.py · query.py · cli.py), the artifact gate and retry/block policy, scope-guard hooks, the agent's system prompts, the trace bridge (kimi_bridge.py) and live dashboard wiring, and this page.
Produced by the agent
(Kimi K3, inside the gates)
Nine bit-accurate golden models, drafted RTL (smx_cache, smx_v_calc, smx_core, weight_stream_ctrl, rc2_hardening), the C driver, 19 task reports, and the one-command regression suite.
Upstream, read-only The Mirage RTL, block diagram and model scripts. The agent could not write into it — enforced by hook, not etiquette.
The workflow dashboard at 127.0.0.1:8787: the harness architecture graph on the left with pipeline, memory-tier and tool zones, and a live trajectory feed on the right listing agent spawns, memory reads and tool calls with millisecond durations.
Fig. 10 — Watched live, not replayed: a local dashboard renders the harness from its own config and streams every spawn, memory read and tool call via the trace bridge (workflow/kimi_bridge.py, traces/*.jsonl).

Stack: Python 3.11 stdlib · Kimi K3 via Kimi Code · ghdl 4.1 / VHDL-2008 · vanilla JS + inline SVG. No framework anywhere in the chain.