[PyCDE] Remove a bunch of old cruft

A lot of this functionality was replaced by the ESI runtime.
This commit is contained in:
John Demme 2024-01-20 03:05:53 +00:00
parent d0879a7663
commit 0cd857f581
10 changed files with 0 additions and 990 deletions

View File

@ -1,102 +0,0 @@
cmake_minimum_required(VERSION 3.13.4)
project(esi_ram_test)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# TODO: most of this stuff should be moved to a .cmake file that is included.
# fetch https://github.com/veselink1/refl-cpp
include(FetchContent)
FetchContent_Declare(
refl-cpp
GIT_REPOSITORY https://github.com/veselink1/refl-cpp
GIT_TAG v0.12.4
)
FetchContent_MakeAvailable(refl-cpp)
# Assert that CIRCT_DIR is defined.
if(NOT DEFINED CIRCT_DIR)
message(FATAL_ERROR "CIRCT_DIR must be defined.")
endif()
if(NOT DEFINED PYCDE_OUT_DIR)
message(FATAL_ERROR "PYCDE_OUT_DIR must be defined.")
endif()
message(STATUS "CIRCT_DIR= ${CIRCT_DIR}")
message(STATUS "PYCDE_OUT_DIR= ${PYCDE_OUT_DIR}")
set(CAPNP_SCHEMA "${PYCDE_OUT_DIR}/hw/schema.capnp")
set(ESI_CPP_API "${PYCDE_OUT_DIR}/hw/ESISystem.h")
set(ESI_HW_INCLUDE_DIR "${PYCDE_OUT_DIR}/hw")
# Ensure that the above files are present
if(NOT EXISTS ${CAPNP_SCHEMA})
message(FATAL_ERROR "CAPNP_SCHEMA not found: ${CAPNP_SCHEMA}")
endif()
if(NOT EXISTS ${ESI_CPP_API})
message(FATAL_ERROR "ESI_CPP_API not found: ${ESI_CPP_API}")
endif()
if(DEFINED CAPNP_PATH)
set(ENV{PKG_CONFIG_PATH}
"${CAPNP_PATH}/lib/pkgconfig:$ENV{PKG_CONFIG_PATH}")
find_package(CapnProto CONFIG PATHS ${CAPNP_PATH})
else()
set(ENV{PKG_CONFIG_PATH}
"${CIRCT_DIR}/ext/lib/pkgconfig:$ENV{PKG_CONFIG_PATH}")
find_package(CapnProto CONFIG PATHS "${CIRCT_DIR}/ext")
endif()
if (NOT CapnProto_FOUND)
message(FATAL_ERROR "Cap'n Proto not found.")
endif()
# Move schema to the build directory - required by capnp_generate_cpp.
set(CAPNPC_SRC_PREFIX ${CMAKE_CURRENT_BINARY_DIR}/capnp_generated)
set(CAPNPC_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/capnp_generated)
file(COPY ${CAPNP_SCHEMA} DESTINATION ${CAPNPC_OUTPUT_DIR})
get_filename_component(CAPNP_SCHEMA_BASENAME ${CAPNP_SCHEMA} NAME)
set(COPIED_CAPNP_SCHEMA ${CAPNP_OUTDIR}/${CAPNP_SCHEMA_BASENAME})
capnp_generate_cpp(
ESI_RAM_SRCS
ESI_RAM_HDRS
${CAPNPC_SRC_PREFIX}/${CAPNP_SCHEMA_BASENAME}
)
message(STATUS "CAPNP_OUTDIR= ${CAPNP_OUTDIR}")
add_executable(esi_ram_test
esi_ram.cpp
${ESI_RAM_SRCS}
${ESI_RAM_HDRS}
)
target_link_libraries(esi_ram_test
${CAPNP_LIBRARIES}
refl-cpp
)
target_include_directories(esi_ram_test PUBLIC
# Include the copied ESI C++ runtime headers.
${PYCDE_OUT_DIR}/runtime/cpp/include)
message("ESI_RAM_SRCS: ${ESI_RAM_SRCS}")
message("ESI_RAM_HDRS: ${ESI_RAM_HDRS}")
target_include_directories(
esi_ram_test
PUBLIC
${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR}
${CAPNP_INCLUDE_DIRS}
${CAPNP_OUTDIR}
${CIRCT_DIR}/include
${ESI_HW_INCLUDE_DIR}
)
target_compile_definitions(
esi_ram_test
PUBLIC
-DESI_COSIM_CAPNP_H=\"${CAPNPC_OUTPUT_DIR}/schema.capnp.h\"
)

View File

@ -1,138 +0,0 @@
// REQUIRES: esi-cosim
// XFAIL: *
// clang-format off
// Create ESI system
// RUN: rm -rf %t
// RUN: %PYTHON% %S/../esi_ram.py %t 2>&1
// Build the project using the CMakeLists.txt from this directory. Just move
// everything to the output folder in the build directory; this is very convenient
// if we want to run the build manually afterwards.
// RUN: cp %s %t
// RUN: cp %S/CMakeLists.txt %t
// RUN: cmake -S %t \
// RUN: -B %t/build \
// RUN: -DCIRCT_DIR=%CIRCT_SOURCE% \
// RUN: -DPYCDE_OUT_DIR=%t
// RUN: cmake --build %t/build
// Run test
// ... can't glob *.sv because PyCDE always includes driver.sv, but that's not the
// top that we want to use. Just delete it.
// RUN: rm %t/hw/driver.sv
// RUN: esi-cosim-runner.py --tmpdir=%t \
// RUN: --no-aux-files \
// RUN: --schema %t/hw/schema.capnp \
// RUN: --exec %t/build/esi_ram_test \
// RUN: %t/hw/*.sv
// To run this test manually:
// 1. run `ninja check-pycde-integration` (this will create the output folder, run PyCDE, ...)
// 2. navigate to %t
// 3. In a separate terminal, run esi-cosim-runner.py in server only mode:
// - cd %t
// - esi-cosim-runner.py --tmpdir=$(pwd) --schema=$(pwd)/hw/schema.capnp --server-only $(pwd)/hw/top.sv $(pwd)
// 4. In another terminal, run the test executable. When running esi-cosim-runner, it'll print the $port which
// the test executable should connect to.
// - cd %t/build
// - ./esi_ram_test localhost:$port ../hw/schema.capn
// clang-format on
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include "esi/backends/capnp.h"
#include ESI_COSIM_CAPNP_H
#include "ESISystem.h"
using namespace esi;
using namespace runtime;
template <typename T>
int logTestFailure(T expected, T actual, int testID) {
std::cerr << "Test " << testID << " failed: expected " << expected << ", got "
<< actual << std::endl;
return testID;
}
template <typename TBackend>
int runTest(TBackend &backend) {
// Connect the ESI system to the provided backend.
esi::runtime::top top(backend);
auto write_cmd =
ESITypes::Struct16871797234873963366{.address = 2, .data = 42};
auto loopback_result = (*top.bsp->loopback)(write_cmd);
if (loopback_result != write_cmd)
return logTestFailure(write_cmd, loopback_result, 1);
auto read_result = (*top.bsp->read)(2);
if (read_result != ESITypes::I64(0))
return logTestFailure(ESITypes::I64(0), read_result, 2);
read_result = (*top.bsp->read)(3);
if (read_result != ESITypes::I64(0))
return logTestFailure(ESITypes::I64(0), read_result, 3);
(*top.bsp->write)(write_cmd);
read_result = (*top.bsp->read)(2);
if (read_result != ESITypes::I64(42))
return logTestFailure(ESITypes::I64(42), read_result, 4);
read_result = (*top.bsp->read)(3);
if (read_result != ESITypes::I64(42))
return logTestFailure(ESITypes::I64(42), read_result, 5);
// Re-write a 0 to the memory (mostly for debugging purposes to allow us to
// keep the server alive and rerun the test).
write_cmd = ESITypes::Struct16871797234873963366{.address = 2, .data = 0};
(*top.bsp->write)(write_cmd);
read_result = (*top.bsp->read)(2);
if (read_result != ESITypes::I64(0))
return logTestFailure(ESITypes::I64(0), read_result, 6);
return 0;
}
int run_cosim_test(const std::string &host, unsigned port) {
// Run test with cosimulation backend.
esi::runtime::cosim::CapnpBackend cosim(host, port);
return runTest(cosim);
}
int main(int argc, char **argv) {
std::string rpchostport;
if (argc != 3) {
// Schema not currently used but required by the ESI cosim tester
std::cerr
<< "usage: esi_ram_test {rpc hostname}:{rpc port} {path to schema}"
<< std::endl;
return 1;
}
rpchostport = argv[1];
// Parse the RPC host and port from the command line.
auto colon = rpchostport.find(':');
if (colon == std::string::npos) {
std::cerr << "Invalid RPC host:port string: " << rpchostport << std::endl;
return 1;
}
auto host = rpchostport.substr(0, colon);
auto port = stoi(rpchostport.substr(colon + 1));
auto res = run_cosim_test(host, port);
if (res != 0) {
std::cerr << "Test failed with error code " << res << std::endl;
return 1;
}
std::cout << "Test passed" << std::endl;
return 0;
}

View File

@ -1 +0,0 @@
config.suffixes.add('.cpp')

View File

@ -8,10 +8,6 @@
include(AddMLIRPython)
# Some of the PyCDE CMake code which is only used in Capnp builds uses newer
# CMake features.
cmake_minimum_required(VERSION 3.21.0)
add_compile_definitions("MLIR_PYTHON_PACKAGE_PREFIX=pycde.circt.")
declare_mlir_python_sources(PyCDESources
@ -39,19 +35,13 @@ declare_mlir_python_sources(PyCDESources
signals.py
ndarray.py
esi.py
esi_api.py
esi_runtime_common.py
fsm.py
testing.py
esi_api.py.j2
Makefile.cosim
bsp/__init__.py
bsp/common.py
bsp/cosim.py
bsp/xrt.py
bsp/EsiXrtPython.cpp
bsp/Makefile.xrt.mk
bsp/xrt_package.tcl
bsp/xrt_api.py
@ -126,32 +116,3 @@ foreach(CFile IN LISTS CollateralFiles)
COMPONENT PyCDE
)
endforeach()
install(TARGETS circt-std-sim-drivers
PUBLIC_HEADER DESTINATION python_packages/pycde/collateral
COMPONENT PyCDE
)
if(ESI_COSIM)
add_dependencies(PyCDE EsiCosimDpiServer)
set_property(TARGET EsiCosimDpiServer PROPERTY INSTALL_RPATH "$ORIGIN")
install(TARGETS EsiCosimDpiServer
RUNTIME_DEPENDENCY_SET EsiCosimDpiServer_RUNTIME_DEPS
DESTINATION python_packages/pycde/collateral
COMPONENT PyCDE
)
install(RUNTIME_DEPENDENCY_SET EsiCosimDpiServer_RUNTIME_DEPS
DESTINATION python_packages/pycde/collateral
PRE_EXCLUDE_REGEXES .*
PRE_INCLUDE_REGEXES capnp kj
COMPONENT PyCDE
)
install(TARGETS MtiPli
DESTINATION python_packages/pycde/collateral
COMPONENT PyCDE
)
install(FILES
"$<TARGET_PROPERTY:EsiCosimCapnp,BINARY_DIR>/CosimDpi.capnp"
DESTINATION python_packages/pycde/collateral/runtime
COMPONENT PyCDE
)
endif()

View File

@ -1,23 +0,0 @@
mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
mkfile_dir := $(dir $(mkfile_path))
VSIM_PATH := $(which vsim)
ifneq ($(VSIM_PATH),)
QUESTA_PATH := $(basename $(VSIM_PATH))
endif
ifneq ($(QUESTA_PATH),)
run_questa:
$(QUESTA_PATH)/vlog hw/*.sv
$(QUESTA_PATH)/vsim driver -c -sv_lib hw/libEsiCosimDpiServer -do "run -all; quit"
endif
VERILATOR_PATH := $(which verilator)
ifneq ($(VERILATOR_PATH),)
SV_SRCS = $(shell ls hw/*.sv | grep -v driver.sv)
VERILATOR_SRCS = $(SV_SRCS) $(mkfile_dir)/hw/*.so hw/*.cpp
run_verilator:
$(VERILATOR_PATH) --cc --top-module top -sv --build --exe --assert $(VERILATOR_SRCS)
LD_LIBRARY_PATH=hw obj_dir/Vtop
endif

View File

@ -1,58 +0,0 @@
#include <cstring>
#include <iostream>
#include <stdexcept>
#include <unistd.h>
#include <vector>
// pybind11 includes
#include "pybind11/pybind11.h"
#include "pybind11/stl.h"
namespace py = pybind11;
// XRT includes
#include "experimental/xrt_bo.h"
#include "experimental/xrt_device.h"
#include "experimental/xrt_ip.h"
#include "experimental/xrt_xclbin.h"
// We don't want to clutter up the symbol space any more than necessary, so use
// an anonymous namespace.
namespace {
uint32_t MagicNumOffset = 16;
uint32_t MagicNumberLo = 0xE5100E51;
uint32_t MagicNumberHi = 0x207D98E5;
uint32_t ExpectedVersionNumber = 0;
class Accelerator {
xrt::device m_device;
xrt::ip m_ip;
public:
Accelerator(const std::string &xclbin_path, const std::string kernel_name) {
m_device = xrt::device(0);
auto uuid = m_device.load_xclbin(xclbin_path);
m_ip = xrt::ip(m_device, uuid, kernel_name);
// Check that this is actually an ESI system.
uint32_t magicLo = m_ip.read_register(MagicNumOffset);
uint32_t magicHi = m_ip.read_register(MagicNumOffset + 4);
if (magicLo != MagicNumberLo || magicHi != MagicNumberHi)
throw std::runtime_error("Accelerator is not an ESI system");
// Check version is one we understand.
if (version() != ExpectedVersionNumber)
std::cerr
<< "[ESI] Warning: accelerator ESI version may not be compatible\n";
}
uint32_t version() { return m_ip.read_register(MagicNumOffset + 8); }
};
} // namespace
PYBIND11_MODULE(esiXrtPython, m) {
py::class_<Accelerator>(m, "Accelerator")
.def(py::init<const std::string &, const std::string &>())
.def("version", &Accelerator::version);
}

View File

@ -1,157 +0,0 @@
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from jinja2 import Environment, FileSystemLoader, StrictUndefined
from io import FileIO
import json
import pathlib
import re
import shutil
from typing import Dict, List
__dir__ = pathlib.Path(__file__).parent
def _camel_to_snake(camel: str):
if camel.upper() == camel:
return camel.lower()
return re.sub(r'(?<!^)(?=[A-Z])', '_', camel).lower()
def _get_ports_for_clients(clients):
# Assemble lists of clients for each service port.
ports = {}
for client in clients:
port = client['port']['inner']
if port not in ports:
ports[port] = []
ports[port].append(client)
return ports
class SoftwareApiBuilder:
"""Parent class for all software API builders. Defines an interfaces and tries
to encourage code sharing and API consistency (between languages)."""
class Module:
"""Bookkeeping about modules."""
def __init__(self, name: str):
self.name = name
self.instances: Dict[str, SoftwareApiBuilder.Module] = {}
self.services: List[Dict] = []
def __init__(self, services_json: str):
"""Read in the system descriptor and set up bookkeeping structures."""
self.services = json.loads(services_json)
self.types: Dict[str, Dict] = {}
self.modules: Dict[str, SoftwareApiBuilder.Module] = {}
# Get all the modules listed in the service hierarchy. Populate their
# 'instances' properly.
for top in self.services["top_levels"]:
top_mod = self._get_module(top["module"][1:])
for svc in top["services"]:
parent: SoftwareApiBuilder.Module = top_mod
for inner_ref in [
(inst["outer_sym"], inst["inner"]) for inst in svc["instance_path"]
]:
m = self._get_module(inner_ref[0])
parent.instances[inner_ref[1]] = m
parent = m
# For any modules which have services, add them as appropriate.
for mod in self.services["modules"]:
m = self._get_module(mod["symbol"])
for svc in mod["services"]:
m.services.append(svc)
def _get_module(self, mod_sym: str):
"""Get a module adding an entry if it doesn't exist."""
if mod_sym not in self.modules:
self.modules[mod_sym] = SoftwareApiBuilder.Module(mod_sym)
return self.modules[mod_sym]
def build(self, os: FileIO, tmpl_file: str):
"""Output the API (in a pre-determined order) via callbacks. Encourages some
level of consistency between language APIs."""
env = Environment(loader=FileSystemLoader(str(__dir__)),
undefined=StrictUndefined)
env.globals.update(camel_to_snake=_camel_to_snake,
get_ports_for_clients=_get_ports_for_clients,
get_type_name=self.get_type_name,
type_str_of=self.get_str_type)
template = env.get_template(tmpl_file)
top_levels = [
self._get_module(t["module"][1:]) for t in self.services["top_levels"]
]
os.write(
template.render(services=self.services,
modules=self.modules.values(),
types=self.types,
tops=top_levels))
def get_type_name(self, type: Dict):
"""Create a name for 'type', record it, and return it."""
if "capnp_name" in type:
name = type["capnp_name"]
else:
name = "".join([c if c.isalnum() else '_' for c in type["mlir_name"]])
self.types[name] = type
return name
def get_str_type(self, type: Dict):
assert False, "unimplemented"
class PythonApiBuilder(SoftwareApiBuilder):
def __init__(self, services_json: str):
super().__init__(services_json)
def build(self, system_name: str, sw_dir: pathlib.Path):
"""Emit a Python ESI runtime library into 'output_dir'."""
libdir = sw_dir / system_name
if not libdir.exists():
libdir.mkdir()
common_file = libdir / "common.py"
shutil.copy(__dir__ / "esi_runtime_common.py", common_file)
# Emit the system-specific API.
main = libdir / "__init__.py"
super().build(main.open("w"), "esi_api.py.j2")
def get_str_type(self, type_dict: Dict):
"""Get a Python code string instantiating 'type'."""
def py_type(type: Dict):
dialect = type["dialect"]
mn: str = type["mnemonic"]
if dialect == "esi" and mn == "channel":
return py_type(type["inner"])
if dialect == "builtin":
if mn.startswith("i") or mn.startswith("ui"):
width = int(mn.strip("ui"))
signed = False
elif mn.startswith("si"):
width = int(mn.strip("si"))
signed = True
if width == 0:
return "VoidType()"
return f"IntType({width}, {signed})"
elif dialect == "hw":
if mn == "struct":
fields = [
f"('{x['name']}', {py_type(x['type'])})" for x in type["fields"]
]
fields_str = ", ".join(fields)
return "StructType([" + fields_str + "])"
assert False, "unimplemented type"
return py_type(type_dict["type_desc"])

View File

@ -1,147 +0,0 @@
{# This template is not generated. #}
# Generated ESI runtime API.
from .common import *
{# Function to list port parameters for the service declaration
constructors. #}
{%- macro port_list(ports) -%}
{% for port in ports -%}
{{-port.name}}_ports: typing.List[Port]{% if not loop.last %}, {% endif %}
{%- endfor %}
{%- endmacro %}
{# List of service declaration classes. E.g.:
```
class HostComms:
def __init__(self, to_host_ports: typing.List[Port], from_host_ports: typing.List[Port], req_resp_ports: typing.List[Port]):
self.to_host = to_host_ports
self.from_host = from_host_ports
self.req_resp = req_resp_ports
def to_host_read_any(self):
for p in self.to_host:
rc = p.read(block=False)
if rc is not None:
return rc
return None
def req_resp_read_any(self):
for p in self.req_resp:
rc = p.read(block=False)
if rc is not None:
return rc
return None
```
#}
{%- for svc in services.declarations %}
class {{ svc['name' ]}}:
def __init__(self, {{ port_list(svc.ports) }}):
{%- for port in svc.ports %}
self.{{port.name}} = {{port.name}}_ports
{%- endfor %}
pass
{% for port in svc.ports if port['to-server-type'] is defined -%}
def {{port.name}}_read_any(self):
for p in self.{{port.name}}:
rc = p.read(blocking_timeout=None)
if rc is not None:
return rc
return None
{% endfor -%}
{% endfor %}
{# "Namespace" for design modules. Section looks roughly like:
```
class DesignModules:
class top:
def __init__(self):
self.mid = DesignModules.Mid()
class Mid:
def __init__(self):
from_host = [
ReadPort(['Producer', 'loopback_in'], write_type=ESITypes.I32)
]
to_host = [
WritePort(['Consumer', 'loopback_out'], read_type=ESITypes.I32)
]
req_resp = [
ReadPort(['LoopbackInOut', 'loopback_inout'], write_type=ESITypes.I32)
WritePort(['LoopbackInOut', 'loopback_inout'], read_type=ESITypes.I16)
]
self.host_comms = HostComms(from_host_ports=from_host, to_host_ports=to_host, req_resp_ports=req_resp)
```
#}
class DesignModules:
{% for mod in modules %}
class {{mod.name}}:
def __init__(self, backend):
self._backend = backend
{%- for inst_name, child_mod in mod.instances.items() %}
self.{{camel_to_snake(inst_name)}} = DesignModules.{{child_mod.name}}(backend.get_child("{{inst_name}}"))
{%- endfor %}
{%- for svc in mod.services %}
{%- set ports = get_ports_for_clients(svc.clients) -%}
{% for port_name, port_clients in ports.items() %}
{{port_name}} = [
{% for pc in port_clients -%}
{% if pc.to_client_type is defined and pc.to_server_type is defined -%}
ReadWritePort({{pc.client_name}}, backend, "{{svc.impl_type}}", read_type=ESITypes.{{get_type_name(pc.to_server_type)}}, write_type=ESITypes.{{get_type_name(pc.to_client_type)}})
{%- elif pc.to_client_type is defined -%}
WritePort({{pc.client_name}}, backend, "{{svc.impl_type}}", write_type=ESITypes.{{get_type_name(pc.to_client_type)}})
{%- elif pc.to_server_type is defined -%}
ReadPort({{pc.client_name}}, backend, "{{svc.impl_type}}", read_type=ESITypes.{{get_type_name(pc.to_server_type)}})
{%- endif -%}
{%- if not loop.last %}, {% endif %}
{%- if not loop.last %}
{% endif -%}
{% endfor %}
]
{%- endfor %}
self.{{camel_to_snake(svc.service)}} = {{svc.service}}(
{%- for pn in ports.keys() -%}{{pn}}_ports={{pn}}{% if not loop.last %}, {% endif %}{% endfor -%}
)
{%- endfor %}
pass
{% endfor %}
{# Types section. Example:`
```
class ESITypes:
I32 = IntType(32, False)
I16 = IntType(16, False)
```
#}
class ESITypes:
{% for type_name, type_dict in types.items() %}
{{type_name}} = {{type_str_of(type_dict)}}
{%- if type_dict.capnp_name is defined %}
{{type_name}}.capnp_name = "{{type_dict.capnp_name}}"
{% endif -%}
{%- endfor %}
pass
{# Instantiate the top modules. E.g.:
```
top = DesignModules.top()
```
#}
{% for top in tops %}
{{top.name}} = lambda backend: DesignModules.{{top.name}}(backend.get_child("{{top.name}}"))
{%- endfor %}

View File

@ -1,324 +0,0 @@
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
import capnp
import os
from pathlib import Path
import time
import typing
class Type:
def __init__(self, width, type_id: typing.Optional[int] = None):
self.type_id = type_id
self.width = width
def is_valid(self, obj) -> bool:
"""Is a Python object compatible with HW type."""
assert False, "unimplemented"
class VoidType(Type):
def __init__(self, type_id: typing.Optional[int] = None):
super().__init__(0, type_id)
def is_valid(self, obj) -> bool:
return obj is None
class IntType(Type):
def __init__(self,
width: int,
signed: bool,
type_id: typing.Optional[int] = None):
super().__init__(width, type_id)
self.signed = signed
def is_valid(self, obj) -> bool:
if self.width == 0:
return obj is None
if not isinstance(obj, int):
return False
if obj >= 2**self.width:
return False
return True
def __str__(self):
return ("" if self.signed else "u") + \
f"int{self.width}"
class StructType(Type):
def __init__(self,
fields: typing.List[typing.Tuple[str, Type]],
type_id: typing.Optional[int] = None):
self.fields = fields
width = sum([ftype.width for (_, ftype) in self.fields])
super().__init__(width, type_id)
def is_valid(self, obj) -> bool:
fields_count = 0
if isinstance(obj, dict):
for (fname, ftype) in self.fields:
if fname not in obj:
return False
if not ftype.is_valid(obj[fname]):
return False
fields_count += 1
if fields_count != len(obj):
return False
return True
return False
class Port:
def __init__(self,
client_path: typing.List[str],
backend,
impl_type: str,
read_type: typing.Optional[Type] = None,
write_type: typing.Optional[Type] = None):
# If a backend doesn't support a particular implementation type, just skip
# it. We don't want to error out on services which aren't being used.
if backend.supports_impl(impl_type):
self._backend = backend.get_port(client_path, read_type, write_type)
else:
self._backend = None
self.client_path = client_path
self.read_type = read_type
self.write_type = write_type
class WritePort(Port):
def write(self, msg=None) -> bool:
assert self.write_type is not None, "Expected non-None write_type"
if not self.write_type.is_valid(msg):
raise ValueError(f"'{msg}' cannot be converted to '{self.write_type}'")
if self._backend is None:
raise ValueError("Backend does not support implementation of port")
return self._backend.write(msg)
class ReadPort(Port):
def read(self, blocking_timeout: typing.Optional[float] = 1.0):
if self._backend is None:
raise ValueError("Backend does not support implementation of port")
return self._backend.read(blocking_timeout)
class ReadWritePort(Port):
def __call__(self,
msg=None,
blocking_timeout: typing.Optional[float] = 1.0) -> typing.Any:
"""Send a message and wait for a response. If 'timeout' is exceeded while
waiting for a response, there may well be one coming. It is the caller's
responsibility to clear the response channel before sending another request
so as to ensure correlation between request and response.
Intended for blocking or synchronous interfaces."""
if not self.write(msg):
raise RuntimeError(f"Could not send message '{msg}'")
return self.read(blocking_timeout)
def write(self, msg=None) -> bool:
assert self.write_type is not None, "Expected non-None write_type"
if not self.write_type.is_valid(msg):
raise ValueError(f"'{msg}' cannot be converted to '{self.write_type}'")
return self._backend.write(msg)
def read(self, blocking_timeout: typing.Optional[float] = 1.0):
return self._backend.read(blocking_timeout)
class _CosimNode:
"""Provides a capnp-based co-simulation backend."""
def __init__(self, root, prefix: typing.List[str]):
self._root: Cosim = root
self._endpoint_prefix = prefix
def supports_impl(self, impl_type: str) -> bool:
"""The cosim backend only supports cosim connectivity implementations."""
return impl_type == "cosim"
def get_child(self, child_name: str):
"""When instantiating a child instance, get the backend node with which it
is associated."""
child_path = self._endpoint_prefix + [child_name]
return _CosimNode(self._root, child_path)
def get_port(self,
client_path: typing.List[str],
read_type: typing.Optional[Type] = None,
write_type: typing.Optional[Type] = None):
"""When building a service port, get the backend port which it should use
for interactions."""
path = ".".join(self._endpoint_prefix) + "." + "_".join(client_path)
ep = self._root._open_endpoint(
path,
write_type=write_type.type_id if write_type is not None else None,
read_type=read_type.type_id if read_type is not None else None)
return _CosimPort(self, ep, read_type, write_type)
class Cosim(_CosimNode):
"""Connect to a Cap'N Proto RPC co-simulation and provide a cosim backend
service."""
def __init__(self, schemaPath, hostPort):
"""Load the schema and connect to the RPC server"""
self._schema = capnp.load(schemaPath)
self._rpc_client = capnp.TwoPartyClient(hostPort)
self._cosim = self._rpc_client.bootstrap().cast_as(
self._schema.CosimDpiServer)
# Find the simulation prefix and use it in our parent constructor.
ifaces = self.list()
prefix = [] if len(ifaces) == 0 else ifaces[0].endpointID.split(".")[:1]
super().__init__(self, prefix)
def load_package(path: os.PathLike):
"""Load a cosim connection from something running out of 'path' package dir.
Reads and parses 'cosim.cfg' from that directory to get the connection
information. Loads the capnp schema from the 'runtime' directory in that
package path."""
path = Path(path)
simcfg = path / "cosim.cfg"
if not simcfg.exists():
simcfg = Path.cwd() / "cosim.cfg"
if not simcfg.exists():
raise RuntimeError("Could not find simulation connection file")
port_lines = filter(lambda x: x.startswith("port:"),
simcfg.open().readlines())
port = int(list(port_lines)[0].split(":")[1])
return Cosim(os.path.join(path, "runtime", "schema.capnp"),
f"{os.uname()[1]}:{port}")
def list(self):
"""List the available interfaces"""
return self._cosim.list().wait().ifaces
def _open_endpoint(self, epid: str, write_type=None, read_type=None):
"""Open the endpoint, optionally checking the send and recieve types"""
for iface in self.list():
if iface.endpointID == epid:
# Optionally check that the type IDs match.
if write_type is not None:
assert iface.sendTypeID == write_type.schema.node.id
else:
assert write_type is None
if read_type is not None:
assert iface.recvTypeID == read_type.schema.node.id
else:
assert read_type is None
openResp = self._cosim.open(iface).wait()
assert openResp.iface is not None
return openResp.iface
assert False, f"Could not find specified EndpointID: {epid}"
class _CosimPort:
"""Cosim backend for service ports. This is where the real meat is buried."""
class _TypeConverter:
"""Parent class for Capnp type converters."""
def __init__(self, schema, esi_type: Type):
self.esi_type = esi_type
assert hasattr(esi_type, "capnp_name")
if not hasattr(schema, esi_type.capnp_name):
raise ValueError("Cosim does not support non-capnp types.")
self.capnp_type = getattr(schema, esi_type.capnp_name)
class _VoidConverter(_TypeConverter):
"""Convert python ints to and from capnp messages."""
def write(self, py_int: None):
return self.capnp_type.new_message()
def read(self, capnp_resp) -> None:
return capnp_resp.as_struct(self.capnp_type)
class _IntConverter(_TypeConverter):
"""Convert python ints to and from capnp messages."""
def write(self, py_int: int):
return self.capnp_type.new_message(i=py_int)
def read(self, capnp_resp) -> int:
return capnp_resp.as_struct(self.capnp_type).i
class _StructConverter(_TypeConverter):
"""Convert python ints to and from capnp messages."""
def write(self, py_dict: dict):
return self.capnp_type.new_message(**py_dict)
def read(self, capnp_resp) -> int:
capnp_msg = capnp_resp.as_struct(self.capnp_type)
ret = {}
for (fname, _) in self.esi_type.fields:
if hasattr(capnp_msg, fname):
ret[fname] = getattr(capnp_msg, fname)
return ret
# Lookup table for getting the correct type converter for a given type.
ConvertLookup = {
VoidType: _VoidConverter,
IntType: _IntConverter,
StructType: _StructConverter
}
def __init__(self, node: _CosimNode, endpoint,
read_type: typing.Optional[Type],
write_type: typing.Optional[Type]):
self._endpoint = endpoint
schema = node._root._schema
# For each type, lookup the type converter and store that instead of the
# type itself.
if read_type is not None:
converter = _CosimPort.ConvertLookup[type(read_type)]
self._read_convert = converter(schema, read_type)
if write_type is not None:
converter = _CosimPort.ConvertLookup[type(write_type)]
self._write_convert = converter(schema, write_type)
def write(self, msg) -> bool:
"""Write a message to this port."""
self._endpoint.send(self._write_convert.write(msg)).wait()
return True
def read(self, blocking_time: typing.Optional[float]):
"""Read a message from this port. If 'blocking_timeout' is None, return
immediately. Otherwise, wait up to 'blocking_timeout' for a message. Returns
the message if found, None if no message was read."""
if blocking_time is None:
# Non-blocking.
recvResp = self._endpoint.recv(False).wait()
else:
# Blocking. Since our cosim rpc server doesn't currently support blocking
# reads, use polling instead.
e = time.time() + blocking_time
recvResp = None
while recvResp is None or e > time.time():
recvResp = self._endpoint.recv(False).wait()
if recvResp.hasData:
break
else:
time.sleep(0.001)
if not recvResp.hasData:
return None
assert recvResp.resp is not None
return self._read_convert.read(recvResp.resp)

View File

@ -15,7 +15,6 @@ from .types import TypeAlias
from . import circt
from .circt import ir, passmanager
from .circt.dialects import esi, hw, msft
from .esi_api import PythonApiBuilder
from contextvars import ContextVar
from collections.abc import Iterable