Skip to content

Commit

Permalink
[ESI][PyCDE] Callback service (#7153)
Browse files Browse the repository at this point in the history
Call software functions from hardware.
  • Loading branch information
teqdruid authored and uenoku committed Jun 20, 2024
1 parent fb2af21 commit 71bb35a
Show file tree
Hide file tree
Showing 19 changed files with 510 additions and 12 deletions.
1 change: 1 addition & 0 deletions frontends/PyCDE/integration_test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ else()
CIRCTPythonModules
ESIRuntime
ESIPythonRuntime
esitester
)

# If ESI Cosim is available to build then enable its tests.
Expand Down
68 changes: 68 additions & 0 deletions frontends/PyCDE/integration_test/esitester.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# ===- esitester.py - accelerator for testing ESI functionality -----------===//
#
# 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
#
# ===----------------------------------------------------------------------===//
#
# This accelerator is intended to eventually grow into a full ESI accelerator
# test image. It will be used both to test system functionality and system
# performance. The corresponding software appliciation in the ESI runtime and
# the ESI cosim. Where this should live longer-term is a unclear.
#
# ===----------------------------------------------------------------------===//

# REQUIRES: esi-runtime, esi-cosim, rtl-sim, esitester
# RUN: rm -rf %t
# RUN: mkdir %t && cd %t
# RUN: %PYTHON% %s %t 2>&1
# RUN: esi-cosim.py -- esitester cosim env | FileCheck %s

import pycde
from pycde import AppID, Clock, Module, Reset, generator
from pycde.bsp import cosim
from pycde.constructs import Wire
from pycde.esi import CallService
from pycde.types import Bits, Channel, UInt

import sys


class PrintfExample(Module):
"""Call a printf function on the host once at startup."""

clk = Clock()
rst = Reset()

@generator
def construct(ports):
# CHECK: PrintfExample: 7
arg_data = UInt(32)(7)

sent_signal = Wire(Bits(1), "sent_signal")
sent = Bits(1)(1).reg(ports.clk,
ports.rst,
ce=sent_signal,
rst_value=Bits(1)(0))
arg_valid = ~sent & ~ports.rst
arg_chan, arg_ready = Channel(UInt(32)).wrap(arg_data, arg_valid)
sent_signal.assign(arg_ready & arg_valid)
CallService.call(AppID("PrintfExample"), arg_chan, Bits(0))


class EsiTesterTop(Module):
clk = Clock()
rst = Reset()

@generator
def construct(ports):
PrintfExample(clk=ports.clk, rst=ports.rst)


if __name__ == "__main__":
s = pycde.System(cosim.CosimBSP(EsiTesterTop),
name="EsiTester",
output_directory=sys.argv[1])
s.compile()
s.package()
1 change: 1 addition & 0 deletions frontends/PyCDE/integration_test/lit.cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@
# Enable ESI runtime tests.
if config.esi_runtime == "ON":
config.available_features.add('esi-runtime')
config.available_features.add('esitester')

llvm_config.with_environment('PYTHONPATH',
[f"{config.esi_runtime_path}/python/"],
Expand Down
5 changes: 4 additions & 1 deletion frontends/PyCDE/src/pycde/constructs.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,15 @@ def ControlReg(clk: Signal,
rst: Signal,
asserts: List[Signal],
resets: List[Signal],
name: Optional[str] = None) -> BitVectorSignal:
name: Optional[str] = None) -> BitsSignal:
"""Constructs a 'control register' and returns the output. Asserts are signals
which causes the output to go high (on the next cycle). Resets do the
opposite. If both an assert and a reset are active on the same cycle, the
assert takes priority."""

assert len(asserts) > 0
assert len(resets) > 0

@modparams
def ControlReg(num_asserts: int, num_resets: int):

Expand Down
39 changes: 39 additions & 0 deletions frontends/PyCDE/src/pycde/esi.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,45 @@ def _op(sym_name: ir.StringAttr):
FuncService = _FuncService()


class _CallService(ServiceDecl):
"""ESI standard service to request execution of a function."""

def __init__(self):
super().__init__(self.__class__)

def call(self, name: AppID, arg: ChannelSignal,
result_type: Type) -> ChannelSignal:
"""Call a function with the given argument. 'arg' must be a ChannelSignal
with the argument value."""
func_bundle = Bundle([
BundledChannel("arg", ChannelDirection.FROM, arg.type),
BundledChannel("result", ChannelDirection.TO, result_type)
])
call_bundle = self.get(name, func_bundle)
bundle_rets = call_bundle.unpack(arg=arg)
return bundle_rets['result']

def get(self, name: AppID, func_type: Bundle) -> BundleSignal:
"""Expose a bundle to the host as a function. Bundle _must_ have 'arg' and
'result' channels going FROM the server and TO the server, respectively."""
self._materialize_service_decl()

func_call = _FromCirctValue(
raw_esi.RequestConnectionOp(
func_type._type,
hw.InnerRefAttr.get(self.symbol, ir.StringAttr.get("call")),
name._appid).toClient)
assert isinstance(func_call, BundleSignal)
return func_call

@staticmethod
def _op(sym_name: ir.StringAttr):
return raw_esi.CallServiceDeclOp(sym_name)


CallService = _CallService()


def package(sys: System):
"""Package all ESI collateral."""

Expand Down
2 changes: 1 addition & 1 deletion frontends/PyCDE/src/pycde/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ class GeneratorCtxt:
def __init__(self, builder: ModuleLikeBuilderBase, ports: PortProxyBase, ip,
loc: ir.Location) -> None:
self.bc = _BlockContext()
self.bb = BackedgeBuilder()
self.bb = BackedgeBuilder(builder.name)
self.ip = ir.InsertionPoint(ip)
self.loc = loc
self.clk = None
Expand Down
16 changes: 16 additions & 0 deletions include/circt/Dialect/ESI/ESIStdServices.td
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,22 @@ def FuncServiceDeclOp : ESI_Op<"service.std.func",
}];
}

def CallServiceDeclOp : ESI_Op<"service.std.call",
[SingleBlock, NoTerminator, HasParent<"::mlir::ModuleOp">,
Symbol, DeclareOpInterfaceMethods<ServiceDeclOpInterface>]> {
let summary = "Service against which hardware can call into software";

let arguments = (ins SymbolNameAttr:$sym_name);

let assemblyFormat = [{
$sym_name attr-dict
}];

let extraClassDeclaration = [{
std::optional<StringRef> getTypeName() { return "esi.service.std.call"; }
}];
}

def MMIOServiceDeclOp: ESI_Op<"service.std.mmio",
[HasParent<"::mlir::ModuleOp">, Symbol,
DeclareOpInterfaceMethods<ServiceDeclOpInterface>]> {
Expand Down
1 change: 1 addition & 0 deletions integration_test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ if (ESI_RUNTIME)
ESIRuntime
ESIPythonRuntime
esiquery
esitester
)

# If ESI Cosim is available to build then enable its tests.
Expand Down
28 changes: 28 additions & 0 deletions integration_test/Dialect/ESI/runtime/callback.mlir
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// REQUIRES: esi-cosim, esi-runtime, rtl-sim, esitester
// RUN: rm -rf %t6 && mkdir %t6 && cd %t6
// RUN: circt-opt %s --esi-connect-services --esi-appid-hier=top=top --esi-build-manifest=top=top --esi-clean-metadata > %t4.mlir
// RUN: circt-opt %t4.mlir --lower-esi-to-physical --lower-esi-bundles --lower-esi-ports --lower-esi-to-hw=platform=cosim --lower-seq-to-sv --lower-hwarith-to-hw --canonicalize --export-split-verilog -o %t3.mlir
// RUN: cd ..
// RUN: esi-cosim.py --source %t6 --top top -- esitester cosim env | FileCheck %s

hw.module @EsiTesterTop(in %clk : !seq.clock, in %rst : i1) {
hw.instance "PrintfExample" sym @PrintfExample @PrintfExample(clk: %clk: !seq.clock, rst: %rst: i1) -> ()
}

// CHECK: PrintfExample: 7
hw.module @PrintfExample(in %clk : !seq.clock, in %rst : i1) {
%0 = hwarith.constant 7 : ui32
%true = hw.constant true
%false = hw.constant false
%1 = seq.compreg.ce %true, %clk, %5 reset %rst, %false : i1
%true_0 = hw.constant true
%2 = comb.xor bin %1, %true_0 : i1
%true_1 = hw.constant true
%3 = comb.xor bin %rst, %true_1 {sv.namehint = "inv_rst"} : i1
%4 = comb.and bin %2, %3 : i1
%chanOutput, %ready = esi.wrap.vr %0, %4 : ui32
%5 = comb.and bin %ready, %4 {sv.namehint = "sent_signal"} : i1
%6 = esi.service.req <@_CallService::@call>(#esi.appid<"PrintfExample">) : !esi.bundle<[!esi.channel<ui32> from "arg", !esi.channel<i0> to "result"]>
%result = esi.bundle.unpack %chanOutput from %6 : !esi.bundle<[!esi.channel<ui32> from "arg", !esi.channel<i0> to "result"]>
}
esi.service.std.call @_CallService
1 change: 1 addition & 0 deletions integration_test/lit.cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@
if config.esi_runtime == "1":
config.available_features.add('esi-runtime')
tools.append('esiquery')
tools.append('esitester')

llvm_config.with_environment('PYTHONPATH',
[f"{config.esi_runtime_path}/python/"],
Expand Down
13 changes: 13 additions & 0 deletions lib/Dialect/ESI/ESIStdServices.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ void RandomAccessMemoryDeclOp::getPortList(
ports.push_back(readPortInfo());
}

void CallServiceDeclOp::getPortList(SmallVectorImpl<ServicePortInfo> &ports) {
auto *ctxt = getContext();
ports.push_back(ServicePortInfo{
hw::InnerRefAttr::get(getSymNameAttr(), StringAttr::get(ctxt, "call")),
ChannelBundleType::get(
ctxt,
{BundledChannel{StringAttr::get(ctxt, "arg"), ChannelDirection::from,
ChannelType::get(ctxt, AnyType::get(ctxt))},
BundledChannel{StringAttr::get(ctxt, "result"), ChannelDirection::to,
ChannelType::get(ctxt, AnyType::get(ctxt))}},
/*resettable=*/UnitAttr())});
}

void FuncServiceDeclOp::getPortList(SmallVectorImpl<ServicePortInfo> &ports) {
auto *ctxt = getContext();
ports.push_back(ServicePortInfo{
Expand Down
7 changes: 7 additions & 0 deletions lib/Dialect/ESI/runtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,13 @@ install(TARGETS esiquery
COMPONENT ESIRuntime
)

# The esitester tool is both an example and test driver. As it is not intended
# for production use, it is not installed.
add_executable(esitester
${CMAKE_CURRENT_SOURCE_DIR}/cpp/tools/esitester.cpp
)
target_link_libraries(esitester PRIVATE ESIRuntime)

# Global variable for the path to the ESI runtime for use by tests.
set(ESIRuntimePath "${CMAKE_CURRENT_BINARY_DIR}"
CACHE INTERNAL "Path to ESI runtime" FORCE)
Expand Down
37 changes: 33 additions & 4 deletions lib/Dialect/ESI/runtime/cpp/include/esi/Accelerator.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
#include <typeinfo>

namespace esi {
// Forward declarations.
class AcceleratorServiceThread;

//===----------------------------------------------------------------------===//
// Constants used by low-level APIs.
Expand Down Expand Up @@ -74,16 +76,20 @@ class Accelerator : public HWModule {
/// subclasses.
class AcceleratorConnection {
public:
AcceleratorConnection(Context &ctxt) : ctxt(ctxt) {}

AcceleratorConnection(Context &ctxt);
virtual ~AcceleratorConnection() = default;
Context &getCtxt() const { return ctxt; }

/// Disconnect from the accelerator cleanly.
void disconnect();

/// Request the host side channel ports for a particular instance (identified
/// by the AppID path). For convenience, provide the bundle type.
virtual std::map<std::string, ChannelPort &>
requestChannelsFor(AppIDPath, const BundleType *) = 0;

AcceleratorServiceThread *getServiceThread() { return serviceThread.get(); }

using Service = services::Service;
/// Get a typed reference to a particular service type. Caller does *not* take
/// ownership of the returned pointer -- the Accelerator object owns it.
Expand Down Expand Up @@ -112,13 +118,15 @@ class AcceleratorConnection {
const HWClientDetails &clients) = 0;

private:
/// ESI accelerator context.
Context &ctxt;

/// Cache services via a unique_ptr so they get free'd automatically when
/// Accelerator objects get deconstructed.
using ServiceCacheKey = std::tuple<const std::type_info *, AppIDPath>;
std::map<ServiceCacheKey, std::unique_ptr<Service>> serviceCache;

/// ESI accelerator context.
Context &ctxt;
std::unique_ptr<AcceleratorServiceThread> serviceThread;
};

namespace registry {
Expand Down Expand Up @@ -149,6 +157,27 @@ struct RegisterAccelerator {

} // namespace internal
} // namespace registry

/// Background thread which services various requests. Currently, it listens on
/// ports and calls callbacks for incoming messages on said ports.
class AcceleratorServiceThread {
public:
AcceleratorServiceThread();
~AcceleratorServiceThread();

/// When there's data on any of the listenPorts, call the callback. Callable
/// from any thread.
void
addListener(std::initializer_list<ReadChannelPort *> listenPorts,
std::function<void(ReadChannelPort *, MessageData)> callback);

/// Instruct the service thread to stop running.
void stop();

private:
struct Impl;
std::unique_ptr<Impl> impl;
};
} // namespace esi

#endif // ESI_ACCELERATOR_H
19 changes: 19 additions & 0 deletions lib/Dialect/ESI/runtime/cpp/include/esi/Common.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include <cstdint>
#include <map>
#include <optional>
#include <stdexcept>
#include <string>
#include <vector>

Expand Down Expand Up @@ -94,6 +95,24 @@ class MessageData {
/// Get the size of the data in bytes.
size_t getSize() const { return data.size(); }

/// Cast to a type. Throws if the size of the data does not match the size of
/// the message. The lifetime of the resulting pointer is tied to the lifetime
/// of this object.
template <typename T>
const T *as() const {
if (data.size() != sizeof(T))
throw std::runtime_error("Data size does not match type size. Size is " +
std::to_string(data.size()) + ", expected " +
std::to_string(sizeof(T)) + ".");
return reinterpret_cast<const T *>(data.data());
}

/// Cast from a type to its raw bytes.
template <typename T>
static MessageData from(T &t) {
return MessageData(reinterpret_cast<const uint8_t *>(&t), sizeof(T));
}

private:
std::vector<uint8_t> data;
};
Expand Down
9 changes: 9 additions & 0 deletions lib/Dialect/ESI/runtime/cpp/include/esi/Ports.h
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,15 @@ class BundlePort {
return channels;
}

/// Cast this Bundle port to a subclass which is actually useful. Returns
/// nullptr if the cast fails.
// TODO: this probably shouldn't be 'const', but bundle ports' user access are
// const. Change that.
template <typename T>
T *getAs() const {
return const_cast<T *>(dynamic_cast<const T *>(this));
}

private:
AppID id;
std::map<std::string, ChannelPort &> channels;
Expand Down
Loading

0 comments on commit 71bb35a

Please sign in to comment.