RISC-V ISA Extension Implementation
===================================
A CHIA case study that uses an LLM-in-the-loop to implement a RISC-V ISA
extension in a `BOOM `_ core and prove it correct
against `Spike `_, the golden
ISA simulator. The full flow lives in ``chia/examples/riscv_extensions``.
Overview
--------
Proposing and evaluating microarchitectural improvements is a fundamental part of computer architecture research. Cross-stack approaches often propose new ISA extensions backed by additional microarchitectural features. Designing the RTL is usually the easy half, leaving thorough verification and PPA analysis (performance and ASIC quality-of-results collection) as the difficult task.
``riscv_extensions`` automates both halves as a loop. For one extension, each run:
#. resets BOOM and lets an LLM implement the extension in Chisel, guided by the
ratified spec that is given as an input;
#. evaluates on **directed tests** — one program per instruction in the extension,
run differentially against Spike;
#. checks functional correctness on the full **base-ISA regression** suite, so the edit can't break anything else;
#. **stress-tests** with programs that riscv-dv generates — a random mix of the
extension's instructions and ordinary code — co-simulated against Spike in
lockstep for several million instructions; and
#. **synthesizes** the BoomTile with and without the extensioh implementation to measure the area/timing (PPA) cost of the extension.
Spike is the oracle throughout. It is compiled *into* the simulator (cospike) and
checks every committed instruction, so a mismatch is caught the cycle it happens —
not inferred from a final result. A divergence in the stress phase hands the
failing trace to a debug LLM, which fixes the RTL; then the verify-then-stress
process restarts, and a fresh clean stress run is the bar to converge.
Results accumulate as per-extension **sweeps** in the log DB: each sweep stores the
BOOM diff that implements the extension, the LLM transcripts, per-test sim logs,
the PPA delta against the unmodified core, and the generated stimulus.
.. note:: **Result**
We implemented and verified three extensions on a MegaBOOM core —
Bit-Manipulation (Zba/Zbb/Zbc/Zbs), scalar Cryptography (Zbk\*/Zkn\*), and
Zicond — each proven against Spike. They deliver ~5.6% and ~3.5% speedups
(plus up to 10× on OpenSSL crypto), with no timing regressions and only modest
area overhead in SkyWater 130nm. Read more in our
`arXiv paper `_.
How it works
------------
The per-run pipeline
~~~~~~~~~~~~~~~~~~~~~~
``run_vext_loop()`` drives one extension from a clean tree to a converged,
synthesized result. Implementing and testing are one step — a regression failure
feeds straight back to the LLM — and the stress phase only runs once both the
directed tests and the regression suite pass::
DRIVER (one extension -> one sweep)
|
+-- reset BOOM + build the per-instruction directed tests
| (from specs//instructions.json)
|
+-- IMPLEMENT [llm worker]
| implement-LLM edits BOOM Chisel -> build the cospike sim ->
| run the directed tests vs Spike; iterate until all pass
|
+-- REGRESSION run the full base-ISA riscv-test suite on the DUT
|
+-- STRESS TEST [gen + cosim nodes]
| riscv-dv generates random programs mixing the extension;
| stream them through lockstep cosims (Spike vs BOOM).
| divergence -> debug-LLM fixes the RTL -> restart verify + stress
|
+-- PPA synth: BoomTile area/slack, baseline vs implemented [vlsi worker]
+-- archive the sweep (diff, transcripts, sim logs, PPA) to the DB
Differential verification against Spike
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Every test — directed or random — is judged the same way: run it on the DUT with
Spike riding inside the simulator, and fail at the first architectural
disagreement. :class:`~chia.chipyard.cosim_node.CosimNode` runs one ELF in
lockstep; the embedded Spike's ISA string comes from the DUT's own config, so the
oracle always decodes exactly what the core claims to implement. No expected
output is ever hand-encoded — the golden model *is* the check.
Directed tests: every instruction, once
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The directed tests are built from ``specs//instructions.json``, the
extension's closed instruction set (generated from `riscv-opcodes
`_). ``isa_tests`` emits one minimal
program per instruction — a handful of executions over random operands — and
cross-compiles each to an ELF. Coverage is 100% by construction: the table *is*
the instruction set, so every instruction is exercised and checked against Spike.
The random stress test
~~~~~~~~~~~~~~~~~~~~~~~~
Directed tests prove each instruction in isolation; the stress phase proves it in
context. riscv-dv generates large random programs that weave the extension's
instructions through ordinary code — hazards, loops, memory traffic — and streams
them through parallel lockstep cosims. Generators fill a pool on the database node
from the run's start; cosims drain it, marking each test ``pending -> passed``. A
single divergence resets the pool, so after the debug LLM fixes the RTL the whole
batch re-runs and must pass clean.
Generating the extension's instructions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
riscv-dv can generate bitmanip, but has no generator for scalar crypto or Zicond.
Each extension therefore names a riscv-dv **custom target** it generates against:
.. code-block:: python
EXTENSIONS = {
"bitmanip": Extension(name="bitmanip", isa_suffix="_zba_zbb_zbc_zbs", ...),
"crypto": Extension(..., isa_suffix="_zbkb_zbkc_zbkx_zknd_zkne_zknh",
dv_target="riscv_dv_target_crypto"),
"zicond": Extension(..., isa_suffix="_zicond",
dv_target="riscv_dv_target_zicond"),
}
For crypto and Zicond the target defines the missing instructions as riscv-dv
custom (``RV64X``) instructions, which the generator node installs into riscv-dv
before generating so they mix into the random stream like any ALU op; bitmanip
uses riscv-dv's native Zb\* support. The SV generation flow needs an Xcelium
license, so the flow also ships a small set of committed prebuilt programs
per extension: ``--prebuilt-stress`` seeds the pool from those instead of
generating, letting the stress phase run with no license.
Distributing work across the cluster
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Each extension pipeline holds one Chipyard build host for the duration of a run
via a placement group, and the LLM's editor tool is pinned to the same bundle so
its edits land in that host's BOOM tree. Generation, cosim, and synthesis fan out
onto their own node types by resource tag — ``gen_to_pool`` onto the riscv-dv
generators, cosims onto ``verilator_run`` nodes, BoomTile synthesis onto a
``vlsi`` worker. The loop ships **this repo's** ``chia`` and example packages to
every worker via Ray ``py_modules``, so workers run the head's checkout regardless
of what their Docker image baked in.
The LLM in the loop
~~~~~~~~~~~~~~~~~~~~
Both the implementing and debugging agents run on an ``llm`` worker via
:class:`~chia.models.claude.ClaudeCodeLLM`, with a bash tool to edit BOOM and a
spec tool to read the ratified text. Sessions resume across turns so an agent can
iterate on its own changes:
.. code-block:: python
ClaudeCodeLLM(
model=LLM_MODEL,
system_message=open(PROMPTS_DIR / prompt_file).read(),
resume_session=True, projects_cwd=None,
extra_cli_args=LLM_EXTRA_ARGS, # ["--effort", "max"]
)
The implement agent (``prompts/system.md``) writes the extension; on a stress
divergence a debug agent (``prompts/debug.md``) gets the failing lockstep trace
and repairs the RTL.
Adding an extension
~~~~~~~~~~~~~~~~~~~~~
One prerequisite: **Spike and the toolchain must already implement the
extension.** Spike is the oracle, and the directed tests are assembled with
``-march=…``, so the standard tools have to know the instructions — the flow
verifies a BOOM implementation, it doesn't invent the ISA.
Then there are three things to add.
**1. Register it** in ``constants.py`` with its ISA-string suffix (and a custom
``dv_target`` only if riscv-dv can't generate it):
.. code-block:: python
"zicond": Extension(name="zicond", isa_suffix="_zicond",
dv_target="riscv_dv_target_zicond"),
**2. Add the spec + instruction table** under ``specs//``:
.. code-block:: text
specs//instructions.json # the closed instruction set (from riscv-opcodes)
specs//spec.md # OR spec.pdf — the spec tool renders a PDF as-is
**3. Add stress collateral only if riscv-dv can't already generate the
extension.** Bitmanip uses riscv-dv's native Zb\*, so there's nothing to add.
Crypto and Zicond don't exist in riscv-dv, so each ships a custom target — copy an
existing ``riscv_dv_target_/`` and edit the four files that matter:
.. code-block:: text
riscv_dv_target_/
riscv_core_setting.sv # enable the RV64X custom group
testlist.yaml # the random program shapes
isa/custom/riscv_custom_instr_enum.sv # one enum name per instruction
isa/custom/rv64x_instr.sv # DEFINE_CUSTOM_INSTR per instruction
isa/custom/riscv_custom_instr.sv # render each to assembly (convert2asm)
(The copy also carries ``isa/custom/rv32x_instr.sv``, a stub — our cores are
64-bit.) Point the registry entry's ``dv_target`` at the new directory.
Everything else — directed tests, the regression suite, stress generation, cosim,
synthesis — is driven off the registry entry.
Setup
-----
Run these from ``/chia`` unless noted.
.. note:: **No licenses? Still runnable.**
Two stages lean on commercial tools — random generation (Xcelium) and PPA
synthesis (Cadence Genus on the open-source Sky130 PDK) — but you need neither
to run the example. Pass ``--prebuilt-stress`` to feed the stress phase from
prebuilt binaries instead of generating, and ``--no-synth`` to skip the
area/timing step. The implement + verify loop runs fully with both off.
The prebuilt binaries are vendored as a git submodule; fetch them once with
``git submodule update --init examples/riscv_extensions/prebuilt_stress``.
**1. Head conda env** — only the head needs it (this installs the ``chia`` command
used below); workers get ``chia`` via Ray ``py_modules`` and the cluster's Docker
images:
.. code-block:: bash
conda create -n chia_env python=3.10.19 && conda activate chia_env
pip install -e .
**2. Cluster** — ``examples/riscv_extensions/clusters/cluster.yaml`` is the
on-prem template; set ``provider.head_ip`` and each node type's
``compatible_ips``. The node types:
.. list-table::
:header-rows: 1
:widths: 16 12 72
* - Node type
- Resource
- Role
* - ``llm``
- ``llm``
- implement / debug LLM (Docker ``chia-claude-code``)
* - ``head_local``
- ``head_local``
- the loop's read-only tools (spec / status / knowledge)
* - ``database``
- ``database``
- durable sweep store + the stress test pool
* - ``build``
- ``chipyard``
- resets BOOM, applies the LLM's edits, builds the cospike DUT
* - ``riscv_build``
- ``riscv_build``
- cross-compiles the directed tests to ELFs
* - ``cosim``
- ``verilator_run``
- lockstep Spike-vs-BOOM co-simulation
* - ``gen``
- ``dv`` / ``xcelium``
- riscv-dv random generation (Xcelium; skip with ``--prebuilt-stress``)
* - ``vlsi``
- ``VLSI`` / ``Syn``
- BoomTile synthesis (skip with ``--no-synth``)
The ``llm`` container bind-mounts your Claude Code config
(``-v ${HOME}/.claude:/home/ray/.claude``). See
:doc:`/user_guides/cluster_config_reference` for the full schema.
.. code-block:: bash
chia up examples/riscv_extensions/clusters/cluster.yaml
**3. Run one extension** — ``single_loop.py`` implements and verifies a single
extension and archives it to the DB. Run the file directly (it puts the example
packages on ``sys.path``):
.. code-block:: bash
chia job submit --working-dir . -- \
python examples/riscv_extensions/single_loop.py --extension bitmanip
Add ``--no-synth`` on a cluster without a synth node, and ``--prebuilt-stress`` to
run the stress phase from committed binaries instead of generating (no Xcelium):
.. code-block:: bash
chia job submit --working-dir . -- \
python examples/riscv_extensions/single_loop.py \
--extension zicond --no-synth --prebuilt-stress
**4. Run several in parallel** — ``multi_loop.py`` fans out one pipeline per
extension and summarizes their PPA:
.. code-block:: bash
chia job submit --working-dir . -- \
python examples/riscv_extensions/multi_loop.py --extensions bitmanip crypto zicond
**5. Tear down** — when the run is done:
.. code-block:: bash
chia down examples/riscv_extensions/clusters/cluster.yaml
Each run archives a sweep under the log DB (``/sweep_/``): the converged
BOOM diff, per-iteration LLM transcripts, directed and stress sim logs, the
baseline-vs-implemented PPA summary, and the generated ``.S`` stimulus.