Quickstart: Say Hello (World) with CHIA

In this brief introduction to using CHIA, we explore 4 ways you can use CHIA to say Hello (World)!

What we will do in this tutorial

As we describe in chia-basics, a CHIA workflow consists of two parts: a flow and a cluster. The flow is a Python script which orchestrates loops and sequences of tasks, and a cluster is the compute substrate on which a flow executes.

In this tutorial, we will design a simple CHIA workflow (both flow and cluster) to say Hello.

First, we will build a very simple cluster (only requiring a single machine, with the option to use multiple) with a few worker nodes from Chipyard, and write a flow which prints “Hello World!” from a worker in the cluster.

Second, we will use that simple cluster to build a verilator simulator of an in-order RocketChip core in Chipyard, compile a C program which prints “Hello World!” for the RocketChip core, and run the program in our Verilator simulator.

Third, we will add an OpenCode AI agent to the cluster and flow, and ask it to say “Hello World!”

Fourth, and finally, we will ask the AI agent to edit our RocketChip’s RTL using an MCP tool. We will ask it to add a line to the RocketChip source which prints to the Verilator output “Hello World!” after 1000 instructions have been committed by the processor.

Requirements

For this tutorial you need a Linux machine with an SSH server. It needs to be able to use public key authentication, and you will need a key set up for SSHing into the machine. This machine must have Docker installed. Additionally, this machine must have CHIA installed.

Optionally, if you have multiple machines available to you on the same LAN (for example, multiple research computers at a single university), there will be sections of this tutorial which allow you to see CHIA’s ability to spread compute across multiple physical machines. Each of these machines must have Docker installed, but does not need CHIA installed.

The chia/examples/hello-world directory contains checkpoints of the code after each step.

0. Project Setup

Start by creating a directory for the project on your machine. Open a shell in that directory and activate the conda environment where you have installed CHIA. We will assume in this tutorial that this environment is named chia_env.

mkdir chia-hello-world
cd chia-hello-world
conda activate chia_env

You should assume that, unless otherwise specified, all commands in this tutorial are run from within the chia_env conda environment and in the project’s directory.

3. Asking an LLM to say Hello

In this step, we add a new worker to the cluster which will provide the OpenCode CLI. OpenCode is a free and open source coding agent which comes with some free to use models. We will ask OpenCode to say “Hello World!”.

First, we will add a new hello_opencode worker type to our cluster.yaml configuration which uses our CHIA provided OpenCode docker image. The following block should be sufficient.

available_node_types:
    # other worker types ...

    hello_opencode:
        resources: {"opencode_creds": 1}
        worker_setup_commands: ["source ~/.bashrc"]
        num_workers: 1
        compatible_ips: [${THIS_MACHINE}]
        docker:
            image: ghcr.io/ucb-bar/chia-opencode:latest
            container_name: "chia-opencode-${USER}"

We can expand our already running cluster with the following command.

chia up --add cluster.yaml

This version of chia up looks at the already running cluster, and compares it to the current configuration in cluster.yaml. CHIA will attempt to bring up any workers which were not yet instantiated or which have died.

Next, we will add a new import and a small block to our flow script which prompts OpenCode using the free big-pickle model.

from chia.base.ChiaFunction import ChiaFunction, get
from chia.chipyard.chisel_build_node import *
from chia.chipyard.verilator_run_node import *
from pathlib import Path
import subprocess
from ray import ObjectRef
from chia.models.opencode import * # New line
# ...
def main():

    get(print_hello_world.chia_remote()) # Already here

    llm = OpenCodeLLM(model="opencode/big-pickle")
    resp : QueryResult = get(llm.prompt.chia_remote(llm,
        "Can you please respond exactly \"Hello World (#3) from OpenCode!\""
    ))
    print("LLM Responded:")
    print(resp.result)

    # Already here
    c_src = Path("helloworld.c").read_text(encoding="utf-8")
    testBinFuture : ObjectRef[bytes] = compile_program.chia_remote(c_src)

You can now submit the job. You can let it run to completion again if you want, but if you want to stop it early, you can use the following. First, give the job a submission id when you submit it:

chia job submit --submission-id RUNSTEP3 --working-dir . -- python hello-world.py

Then, after you’ve seen the LLM’s response, you can stop tailing the log of the run with CRTL+C and stop the job completely by running the following command:

chia job stop RUNSTEP3

Note that the job will continue running to completion unless you explicitly stop it.

In the output of this run, you should now see the following lines:

(print_hello_world pid=1057797) Hello World (#1) from a remote call!
LLM Responded:
Hello World (#3) from OpenCode!
Output of Verilator run (run_output.log):
Hello World (#2) from Verilator!
[UART] UART0 is here (stdin/stdout).
- /home/ray/chipyard/sims/verilator/generated-src/chipyard.harness.TestHarness.RocketConfig/gen-collateral/TestDriver.v:179: Verilog $finish

4. Editing RTL to say Hello

Finally, we get to tie all of this together with a nice demonstration of a very natural use-case for CHIA: LLMs writing RTL. Specifically, we are going to ask an LLM to have our RocketChip core print “Hello World (#4) from instruction 1000!” when the core has retired it’s 1000th instruction.

In our flow, let’s start with the following new import, the BashTool.

from chia.base.ChiaFunction import ChiaFunction, get
from chia.chipyard.chisel_build_node import *
from chia.chipyard.verilator_run_node import *
from pathlib import Path
import subprocess
from ray import ObjectRef
from chia.models.opencode import *
from chia.base.tools.BashTool import * # New line
# ...

In CHIA, just like any function can be a node, any function can be made into an MCP tool, by registering that function with an object of type ChiaTool. In this tutorial we do not create any new custom MCP tools. Any ChiaTool can be passed to any agent/LLM prompts in CHIA.

A BashTool is an MCP tool (its a child class of ChiaTool) which provides an agent/LLM with a bash interface to a specific worker.

We can instantiate the BashTool like this. We give it the name “chipyard_bash”, set it’s working directory to CHIPYARD_PATH, and set it’s task options so that it lands on our hello_chipyard worker by specifying that it requires a chipyard resource.

def main():
    get(print_hello_world.chia_remote())

    llm = OpenCodeLLM("opencode/big-pickle")
    resp : QueryResult = get(llm.prompt.chia_remote(
        llm, "Can you please respond just \"Hello World from OpenCode!\""))
    print("LLM Responded:")
    print(resp.result)

    # ====== New code starts here ======
    chipyBash : BashTool = BashTool(
        "chipyard_bash", CHIPYARD_PATH,
        task_options={"resources": {"chipyard": 1}}
    )

Let’s write a prompt for the LLM and query it, passing in our chipyTool to the tools argument:

LLM_RTL_PROMPT = "" \
"Can you use the chipyard_bash tool to " \
"edit chipyard's generators/rocket-chip " \
"Chisel source and add a new print to the " \
"Chisel which says \"Hello World (#4) from " \
"instruction 1000!\" when instret reaches " \
"1000? If any message already exists, add " \
"the word \"again\" at the end of the existing " \
"message. Please respond explaining your change."

def main():
    # ...

    chipyBash : BashTool = BashTool(
        "chipyard_bash", CHIPYARD_PATH,
        task_options={"resources": {"chipyard": 1}}
    )
    resp : QueryResult = get(llm.prompt.chia_remote(
        llm, LLM_RTL_PROMPT, tools=[chipyBash]
    ))
    chipyBash.stop()
    print(resp.result)

Note that it’s very important in this example to stop chipyBash because it consumes a chipyard resource. If you leave it running, the other nodes which use chipyard resources will be starved.

Finally, let’s parse the output of the Verilator run to check for the LLM’s change.

# Already Here
verilator_node = VerilatorRunNode()
run_output = get(verilator_node.run.chia_remote(verilator_node, buildFuture, get(testBinFuture), "helloworld.riscv", "/home/ray/verilator/"))

# Already Here
print("Output of Verilator run (run_output.log): ")
print(run_output.log)

# ===== New =====
# First line of run_output.out is "testing $random ..."
# Next 1000 lines are committed instruction trace
# 1002 line should be "Hello from instruction 1000!"
print("Parsed lines from simulation (run_output.out): ")

print(run_output.out.split('\n')[1001])
print(run_output.out.split('\n')[1002]) # Should have LLM's addition
print(run_output.out.split('\n')[1003])

Now, if all goes well, you should see in your output the following

(print_hello_world pid=1076432) Hello World (#1) from a remote call!
LLM Responded:
Hello World (#3) from OpenCode!

Followed by many messages related to the tool MCP server

(_ToolServerActor pid=6063, ip=172.17.0.1) INFO:     Started server process [6063]
(_ToolServerActor pid=6063, ip=172.17.0.1) INFO:     Waiting for application startup.
(_ToolServerActor pid=6063, ip=172.17.0.1) INFO:     Application startup complete.
(_ToolServerActor pid=6063, ip=172.17.0.1) INFO:     Uvicorn running on http://172.17.0.1:8000 (Press CTRL+C to quit)
chipyard_bash started at 172.17.0.1:8000 on node 07348f9a70938e126620044cda1e3f41554de0ad19fac5e54e9ead6e
(_ToolServerActor pid=6063, ip=172.17.0.1) INFO:     ${IP ADDRESS} - "POST /chipyard_bash/mcp HTTP/1.1" 200 OK
(_ToolServerActor pid=6063, ip=172.17.0.1) INFO:     ${IP ADDRESS} - "POST /chipyard_bash/mcp HTTP/1.1" 202 Accepted
(_ToolServerActor pid=6063, ip=172.17.0.1) INFO:     ${IP ADDRESS} - "GET /chipyard_bash/mcp HTTP/1.1" 200 OK
...

Followed, eventually, by a response from the LLM, a tool shutdown message, and finally the verilator outputs. It should look something like this (though the LLM’s response will vary).

The change has been made successfully and compiles without errors. Here's a summary of what was done:

## Change Summary

**File modified:** `/home/ray/chipyard/generators/rocket-chip/src/main/scala/rocket/CSR.scala`

**What was added:** A Chisel `printf` statement that fires when the architecture counter `instret` (instruction retired count) reaches exactly 1000.

**The exact code added** (line 594, right after the `reg_instret` counter definition):

```scala
when (reg_instret === 1000.U) { printf("Hello World (#4) from instruction 1000!\n") }
```

**How it works:**
- The `reg_instret` signal is a `WideCounter(64, io.retire, ...)` that counts each retired instruction (line 593).
- The `when` block checks if `reg_instret` equals exactly `1000.U` (unsigned 1000).
- When the condition is true, Chisel's `printf` (which prints during simulation/emulation) outputs `"Hello World (#4) from instruction 1000!"`.
- Since no existing "Hello World" message was found in the file, the message was added fresh (no "again" suffix needed).
(_ToolServerActor pid=6063, ip=172.17.0.1) INFO:     Waiting for application shutdown.
(_ToolServerActor pid=6063, ip=172.17.0.1) INFO:     Application shutdown complete.
(_ToolServerActor pid=6063, ip=172.17.0.1) INFO:     Finished server process [6063]
Output of Verilator run (run_output.log):
Hello World (#2) from Verilator!
[UART] UART0 is here (stdin/stdout).
- /home/ray/chipyard/sims/verilator/generated-src/chipyard.harness.TestHarness.RocketConfig/gen-collateral/TestDriver.v:179: Verilog $finish

Parsed lines from simulation (run_output.out):
C0:       2574 [1] pc=[0000000080000562] W[r 8=00000000800016c7][1] R[r10=00000000800016c0] R[r 0=0000000000000000] inst=[00750413] addi    s0, a0, 7
Hello World (#4) from instruction 1000!
C0:       2575 [1] pc=[0000000080000566] W[r 8=00000000800016c0][1] R[r 8=00000000800016c7] R[r 0=0000000000000000] inst=[00009861] c.andi  s0, -8

Where the line “Hello World (#4) from instruction 1000!” indicates that the LLM succeeded.

And with that, you’ve gotten CHIA to say Hello in 4 different ways! Thanks!