In the previous post, we built the top of a custom accelerator stack. A PyTorch PrivateUse1 backend intercepted selected tensor operations, converted them into hardware-shaped requests, and kept the application-facing code looking like ordinary PyTorch.
That is only one side of the problem. A useful accelerator also needs an operating system, driver, memory map, interconnect, and hardware that agree on the same contract. The physical SoC or FPGA may not exist when the first software has to be written.
This is where Renode becomes especially useful. It lets us assemble a virtual SoC, boot real target software, attach custom RTL, and exercise the complete path much earlier than a board-first workflow would allow.
Why simulate the complete system early
Testing the RTL in isolation verifies the accelerator's logic and interface. It does not answer whether Linux discovers it, whether the driver binds to the right device-tree node, whether software and hardware agree on register offsets, or whether the complete request survives every boundary between the model and the datapath.
Renode gives us a place to answer those questions before the physical platform is ready. In our experiment it models the RISC-V CPU, memory, interrupt controllers, UARTs, and the rest of the SoC at a functional level. The custom Vector Adder Machine is attached as a Verilator co-simulated peripheral, so only the RTL we are actively developing pays the cost of cycle-level simulation. This mixed level of abstraction is the key: simulating the entire SoC as RTL would be far too slow for a normal software loop, while replacing the accelerator with a purely functional model would hide the hardware integration we want to test.
The result is not a performance model of the final chip; it is a realistic integration model. We can boot the target architecture, load the real kernel module, use the intended device interface, and observe software driving the actual RTL. Because the platform is deterministic and reproducible, we can use the same setup locally and in CI, replay failures without reserving a board, and revise address maps or peripherals while those changes are still cheap.
How the stack fits together
We already covered the individual tools in our open-source RTL workflow. What matters here is that they all meet at the same hardware/software contract.
SystemRDL defines the registers, PeakRDL generates the AXI4-Lite interface, and the Linux driver accesses that interface through MMIO. Verilator executes the Chisel-generated accelerator, while Renode provides the RISC-V SoC around it. The same register map therefore connects verification, RTL, the driver, and the simulated system.
Following one operation through the simulated SoC
We will use the PyTorch custom device example as a concrete example of this workflow. The hardware operation is intentionally modest—addition or subtraction of two four-element vectors—so the surrounding framework, driver, SoC, and RTL integration remain easy to inspect.
When PyTorch dispatches one of those operations, the C++ backend creates a request with the opcode, tensor length, and buffers. Because the target system is simulated, running the complete PyTorch framework inside it would spend too much time emulating software rather than testing the accelerator. Keeping PyTorch native on the host gives us a faster development loop, while a TCP-connected UART and guest relay forward each request to the real ioctl() interface.
From that point onward, the path looks like a real target. The kernel driver validates the request, writes the operands into the SystemRDL-defined registers over AXI4-Lite, clears status, writes the instruction, waits for completion, and reads the result. Those bus transactions reach the Verilated Chisel block attached to the Renode system bus. The result then travels back through the same driver and backend layers until PyTorch receives an ordinary tensor.
From individual operations to a computational graph
Our prototype uses PyTorch's eager execution model: an ATen operation reaches the backend and becomes one hardware request. This is useful during bring-up, but it does not scale efficiently because every operation requires a separate host-to-accelerator request.
For larger workloads, the usual approach is to represent the model as a computational graph and compile groups of operations into larger units of work for the accelerator. This reduces host involvement and lets the compiler optimize execution and data movement across multiple operations. The hardware does not execute the graph directly; it executes the program or command sequence produced from it.
Moving our experiment in that direction would require more than widening the vector unit. We would need a graph compiler or backend delegate, a command processor, DMA, local memory, asynchronous execution, and a driver interface capable of submitting larger pieces of work. Renode would let us develop those additions alongside the software that controls them, while the physical accelerator is still evolving.
Scaling toward real AI workloads
The same full-stack method can grow toward much larger designs. For YOLO-style vision models, the custom hardware would need convolution or matrix engines, activation and quantization support, tensor tiling, DMA, and enough local memory to reuse data efficiently. The graph compiler would decide which regions belong on that hardware and how tensors move between accelerated and CPU operations.
World-model systems expand the problem further. Perception, state estimation, prediction, and planning may have different compute and memory characteristics, so the useful accelerator may be a set of specialized blocks rather than one universal engine. A graph-level representation makes those partitions and their data dependencies visible before they are lowered to hardware commands.
The same method could even be applied to small-parameter LLMs. These models have similar requirements at a different scale: matrix multiplication, quantized weights, attention-related operations, KV-cache management, and memory bandwidth dominate the architecture. The four-word unit in this experiment does none of that, but the surrounding pattern remains valid. PyTorch can describe the model, a compiler can lower supported graph regions, the runtime and driver can submit work, and Renode can provide the SoC in which the complete software and RTL stack is tested.
The value of the prototype is therefore not its arithmetic width. It is that every layer already has a place. We can replace a toy datapath with a serious accelerator without waiting until the end of the project to discover how the framework, compiler, driver, registers, interconnect, operating system, and hardware fit together.