Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Arc] Introduce a Python simulation script generator for arcilator #7942

Open
gtxzsxxk opened this issue Dec 4, 2024 · 3 comments
Open

[Arc] Introduce a Python simulation script generator for arcilator #7942

gtxzsxxk opened this issue Dec 4, 2024 · 3 comments

Comments

@gtxzsxxk
Copy link
Contributor

gtxzsxxk commented Dec 4, 2024

Hi,

In #7929 , I refactored the C++ header generator Python script for the C++ code that wants to call the LLVM IR produced by arcilator. The PR mainly refactors its structure, paving the way for future modularization.

The reason for making it modular is that the script contains some core functions that can serve as a reusable library for other scripts. My proposals are as follows:

  1. Splitting the C++ header generator script

    • Core module: Extract the generic logic for interacting with arcilator's state json and encapsulate it into a standalone Python module.
    • Header file generator: Import core module and only do things around generating C++ headers.
  2. Introducing a Python simulation script generator

    • Develop a Python script that utilizes the core module to generate Python simulation scripts. These scripts will use llvmlite to execute the LLVM IR generated by arcilator.
    • The generated scripts will provide a simulation interface inspired by Chisel, including poke and peek methods. The generated scripts is like the generated C++ header and is used by other testbenches written in Python.
    • Possible use cases: Enable rapid functional validation of circuits with minimal effort, leveraging Python’s expressive syntax for quick and intuitive simulations. With a Python interface, users could efficiently automate simulation workflows and write high-level test scripts.

Before proceeding, I would greatly appreciate your feedback on this proposal.

Thank you for your time and guidance!

Regards

@uenoku
Copy link
Member

uenoku commented Dec 5, 2024

The generated scripts will provide a simulation interface inspired by Chisel, including poke and peek methods. The generated scripts is like the generated C++ header and is used by other testbenches written in Python.

One possible direction is to implement industry standard VPI. By doing so we should be able to execute arc model from frameworks like cocotb relatively easily.

@gtxzsxxk
Copy link
Contributor Author

gtxzsxxk commented Dec 6, 2024

Hi,

One possible direction is to implement industry standard VPI

To me, it might be what acrilator is expected to implement, i.e. emitting some IR that calls callback functions in certain conditions.

be able to execute arc model from frameworks like cocotb relatively easily.

This seems too heavy and beyond what I want to do. If we want the acrilator to be one of the backends in cocotb, we might need to modify arcilator itself. However, my idea mainly focuses on forking the C++ header generator, providing a way for Python code that wants to call the arcilator's artifacts as well. The generated Python code will execute the produced IR with llvmlite in a single thread like the existing C++ arcilator simulation code. So cocotb's coroutine and simulation framework seems having limited usage in this situation.

Thanks for your suggestions!

@gtxzsxxk
Copy link
Contributor Author

Hi,

My idea may not be clear enough. Let me present an example. A multiplexer:

module mux_2to1(
    input [2:0] a,
    input [3:0] b,
    input [1:0] sel,
    output [2:0] out
);
    assign out = sel[0] ? b[3:1] : (sel[1] ? a : 3'd2);
endmodule

We can generate the python testbench like this:

from enum import Enum, auto
import ctypes
import llvmlite

llvmlite.opaque_pointers_enabled = True
import llvmlite.binding as llvm


class SignalType(Enum):
  INPUT = auto()
  OUTPUT = auto()
  REGISTER = auto()
  MEMORY = auto()
  WIRE = auto()


class Signal:
  def __init__(self, name: str, offset: int, num_bits: int, signal_type: SignalType):
    self.name = name
    self.offset = offset
    self.num_bits = num_bits
    self.type = signal_type


class mux_2to1Layout:
  name = "mux_2to1"
  num_states = 4
  num_state_bytes = 4
  io = [Signal("a", 0, 3, SignalType.INPUT),
        Signal("b", 1, 4, SignalType.INPUT),
        Signal("sel", 2, 2, SignalType.INPUT),
        Signal("out", 3, 3, SignalType.OUTPUT)]


class mux_2to1View:
  class __io_in_op_uint8:
    def __init__(self, ptr, offset):
      self.__ptr = ptr
      self.__offset = offset

    def poke(self, data: int):
      self.__ptr[self.__offset] = data

    def peek(self):
      return self.__ptr[self.__offset]

  class __io_out_op_uint8:
    def __init__(self, ptr, offset):
      self.__ptr = ptr
      self.__offset = offset

    def poke(self, data: int):
      raise TypeError("This port is read-only")

    def peek(self):
      return self.__ptr[self.__offset]

  def __init__(self):
    self.storage = ctypes.create_string_buffer(4)
    self.a = self.__io_in_op_uint8(ctypes.cast(ctypes.pointer(self.storage),
                                               ctypes.POINTER(ctypes.c_uint8)), 0)
    self.b = self.__io_in_op_uint8(ctypes.cast(ctypes.pointer(self.storage),
                                               ctypes.POINTER(ctypes.c_uint8)), 1)
    self.sel = self.__io_in_op_uint8(ctypes.cast(ctypes.pointer(self.storage),
                                                 ctypes.POINTER(ctypes.c_uint8)), 2)
    self.out = self.__io_out_op_uint8(ctypes.cast(ctypes.pointer(self.storage),
                                                  ctypes.POINTER(ctypes.c_uint8)), 3)


class mux_2to1:
  llvm_ir = """
    ; ModuleID = 'LLVMDialectModule'
    source_filename = "LLVMDialectModule"

    define void @mux_2to1_eval(ptr %0) {
      %2 = getelementptr i8, ptr %0, i32 2
      %3 = load i2, ptr %2, align 1
      %4 = getelementptr i8, ptr %0, i32 1
      %5 = load i4, ptr %4, align 1
      %6 = load i3, ptr %0, align 1
      %7 = trunc i2 %3 to i1
      %8 = lshr i4 %5, 1
      %9 = trunc i4 %8 to i3
      %10 = lshr i2 %3, 1
      %11 = trunc i2 %10 to i1
      %12 = select i1 %11, i3 %6, i3 2
      %13 = select i1 %7, i3 %9, i3 %12
      %14 = getelementptr i8, ptr %0, i32 3
      store i3 %13, ptr %14, align 1
      ret void
    }
    """

  def __init__(self):
    if int(llvmlite.__version__.split('.')[1]) < 44:
      raise ImportError("The version of llvmlite must greater than or equal to 0.44")

    self.view = mux_2to1View()

    llvm.initialize()
    llvm.initialize_native_target()
    llvm.initialize_native_asmprinter()

    def create_execution_engine():
      """
      Create an ExecutionEngine suitable for JIT code generation on
      the host CPU.  The engine is reusable for an arbitrary number of
      modules.
      """
      # Create a target machine representing the host
      target = llvm.Target.from_default_triple()
      target_machine = target.create_target_machine()
      # And an execution engine with an empty backing module
      backing_mod = llvm.parse_assembly("")
      eng = llvm.create_mcjit_compiler(backing_mod, target_machine)
      return eng

    def compile_ir(engine_, llvm_ir_):
      """
        Compile the LLVM IR string with the given engine.
        The compiled module object is returned.
        """
      # Create a LLVM module object from the IR
      mod_ = llvm.parse_assembly(llvm_ir_)
      mod_.verify()
      # Now add the module and make sure it is ready for execution
      engine_.add_module(mod_)
      engine_.finalize_object()
      engine_.run_static_constructors()
      return mod_

    self.engine = create_execution_engine()
    self.mod = compile_ir(self.engine, self.llvm_ir)

    # Look up the function pointer (a Python int)
    self.func_ptr = self.engine.get_function_address("mux_2to1_eval")

    # Run the function via ctypes
    self.__eval_func = ctypes.CFUNCTYPE(None, ctypes.POINTER(ctypes.c_uint8))(self.func_ptr)
    self.eval_param = ctypes.cast(ctypes.byref(self.view.storage, 0),
                                  ctypes.POINTER(ctypes.c_uint8))

  def eval(self):
    self.__eval_func(self.eval_param)


if __name__ == "__main__":
  dut = mux_2to1()
  dut.view.a.poke(4)
  dut.view.b.poke(5)
  dut.view.sel.poke(2)
  dut.eval()
  print(dut.view.out.peek())
  assert dut.view.out.peek() == 4
  dut.view.a.poke(5)
  dut.view.b.poke(7)
  dut.view.sel.poke(1)
  dut.eval()
  print(dut.view.out.peek())
  assert dut.view.out.peek() == 3
  dut.view.a.poke(5)
  dut.view.b.poke(7)
  dut.view.sel.poke(0)
  dut.eval()
  print(dut.view.out.peek())
  assert dut.view.out.peek() == 2

The output of arcilator is hardcoded in this python module, and can be used by other python code by import, or directly write testbench in if __name__ == "__main__":

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants