Example: Agentic RoCC Accelerator (MemCpy)
A worked, end-to-end example of an agentic hardware-design loop: an LLM
(Claude Code by default; OpenCode or Google Antigravity via --llm) designs
a RISC-V RoCC accelerator in Chisel, CHIA builds it
into a MegaBoom SoC, runs it against a bare-metal test, and, on any failure,
feeds the error back to the LLM to debug and retry until the test passes.
The example lives at examples/memcpy/ and is essentially self-contained: it
depends only on the installed chia package and the shared DRAMSim2 ini files
in examples/common/dramsim_ini. It is a good starting point for building your
own generate, build, simulate, debug loops on top of CHIA’s chipyard nodes.
What it does
The accelerator is a hardware memcpy: it copies an array of 64-bit elements
from a source region to a destination region, driven by two custom
instructions on opcode custom1. The target design is
MegaBoomV3HumanCommitLogConfig (MegaBoom V3 with the human-readable
commit-log harness, so a failing run yields a readable instruction trace for
the debugger).
The loop runs two nodes in parallel, then builds, runs, and debugs:
test build (parallel) implement (parallel)
copy memcpy.c into Claude writes memcpy.scala
$chipyard/tests, run cmake (the RoCC accelerator) and
-> build/memcpy.riscv wires it into the target
build/memcpy.dump config, via chipyard_bash
└──────────────┬──────────────┘
▼
chisel build (ChiselBuildNode) target: MegaBoomV3HumanCommitLogConfig
▼
verilator run (VerilatorRunNode) memcpy.riscv, +loadmem +verbose
▼
build failed / sim failed / incorrect?
│ yes (≤ NUM_DEBUG_ATTEMPTS) │ no
▼ ▼
debug (Claude) ── rebuild + rerun DONE (passed)
Components
File |
Role |
|---|---|
|
Main orchestration: parallel test-build + implement, then the
build → run → debug loop. Dumps all collateral to |
|
|
|
The implement + debug LLM nodes (Claude Code, OpenCode or Antigravity,
chosen with |
|
Run-outcome classification ( |
|
The implement ( |
|
Every tunable knob (loop counts, configs, paths, timeouts, resources). |
|
Minimal cluster: one chisel-build, one verilator, and one node per LLM
backend ( |
|
The bare-metal test: issues the two RoCC instructions and checks the copy. |
The accelerator contract
memcpy.c drives the accelerator at opcode custom1 with two
instructions; the implement prompt specifies exactly this so the generated
design matches the test:
funct == 0—rs1= source base address,rs2= destination base address. Latch both; writerd = 1.funct == 1—rs1= array length (number of 64-bit elements). Copy the whole array source → destination via the RoCC memory port; writerd = 1when done.
Correctness is judged from the MEMCPY Num Correct: N line the program
prints: the run passes iff N == DATA_SIZE (constants.DATA_SIZE, kept in
sync with memcpy.c).
Running it
The bundled cluster.yaml brings up a chisel-build (chipyard) node, a
verilator (verilator_run) node, and one node per LLM backend. The
implement/debug LLM can be Claude Code (llm node), OpenCode
(opencode node) or Google Antigravity (antigravity node, Gemini via
the agy CLI) — keep them up and pick per run with --llm (default
claude), or comment out the ones you don’t use. Each backend reads its
credentials from the mounts shown at the top of cluster.yaml (Antigravity
mounts your ~/.gemini sign-in; choose the global location at sign-in so
Gemini Pro is available). See Authenticating Gemini backends: Antigravity and OpenCode + Vertex AI for the sign-in and mount
details of the two Gemini backends.
export THIS_MACHINE="your_machine_ip"
chia up examples/memcpy/cluster.yaml
# pick the backend with --llm (claude is the default); run from the repo root:
chia job submit -- python $PWD/examples/memcpy/memcpy_loop.py --llm claude
chia job submit -- python $PWD/examples/memcpy/memcpy_loop.py --llm opencode
chia job submit -- python $PWD/examples/memcpy/memcpy_loop.py --llm antigravity
chia down examples/memcpy/cluster.yaml
Ray jobs do not inherit your shell environment, so MEMCPY_* knobs go through
the job’s runtime env. OpenCode is provider-agnostic (MEMCPY_OPENCODE_MODEL
is any provider/model); Gemini on Vertex AI is the wired-up example — it needs
Google ADC on the host and a GCP project:
export GOOGLE_CLOUD_PROJECT=<project> # before `chia up` (the opencode node forwards it)
chia job submit \
--runtime-env-json "{\"env_vars\": {\"MEMCPY_OPENCODE_MODEL\": \"google-vertex/gemini-3.1-pro-preview\", \"GOOGLE_CLOUD_PROJECT\": \"$GOOGLE_CLOUD_PROJECT\"}}" \
-- python $PWD/examples/memcpy/memcpy_loop.py --llm opencode
Pass the absolute path to memcpy_loop.py. The driver runs on the cluster head,
where the repo lives, so out/ is written into the real
examples/memcpy/out.
All backends edit the chipyard checkout through the same chipyard_bash MCP
tool. prompt is a ChiaFunction on each backend, so the loop dispatches it
onto the chosen worker (llm, opencode or antigravity). Claude Code
and Antigravity thread their session transcript from each call into the next so
the debugger resumes the implement conversation; session persistence for
OpenCode is in development (each OpenCode call is currently independent).
Tunable parameters
All knobs live in constants.py; container paths and cluster knobs are
MEMCPY_* environment-overridable, so nothing is hardcoded into the loop.
Constant |
Meaning |
|---|---|
|
Max debug-and-retry rounds after the first failure (default 3). |
|
Reserved for a future post-correctness performance-optimization phase (defined, not yet used). |
|
Chisel config to build ( |
|
Element count; must match |
|
Simulation caps so a hung design fails fast. |
|
Ray scheduling tokens, sized so the persistent |
Debug feedback
On a failure the debug node (the same LLM session, resumed for Claude Code and Antigravity) receives:
Build failure — build stderr tail plus stdout windowed on the first
error.Simulation failure (runtime / timeout / incorrect) — the simulator stdout (the commit log) and spike-dasm output tails, plus the last
COMMIT_LOG_TAIL_LINESlines of the commit log and the lastDUMP_TAIL_LINESlines ofmemcpy.dump(the test disassembly).
Output and per-iteration diffs
Every node result and piece of collateral is written to out/, each filename
prefixed with the timestamp at the moment it is written (so files sort by when
they were produced):
20260626_144501_implement.md
20260626_144502_test_build_memcpy.riscv
20260626_144502_test_build_memcpy.dump
20260626_144503_chisel_diff_attempt0.diff # chipyard diff for this iteration
20260626_144503_chisel_diff_attempt0.json # per-repo diff dict
20260626_145012_chisel_build_attempt0.stdout.txt
20260626_145230_verilator_run_attempt0.log
20260626_145231_feedback_attempt1.md
20260626_145950_debug_attempt1.md
20260626_153044_summary.json
Each iteration’s Chisel diff is captured with collect_diff (in
helpers.py) just before that attempt’s build, so it reflects the exact
source built from the implement node’s work on attempt 0, and the cumulative
implement + debug edits on later attempts.