[ESI] Bidirectional channel requests and Cosim support (#3705)

Allow users to request bidirectional channels (InOut). Decomposes into
one `to_server` and one `to_client` request. Cosim will detect that and
wire them up to the same endpoint.
This commit is contained in:
John Demme 2022-08-11 18:25:55 -07:00 committed by GitHub
parent a6275fe60a
commit dbbc205849
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 227 additions and 35 deletions

View File

@ -8,8 +8,8 @@ class LoopbackTester(esi_cosim.CosimBase):
"""Provides methods to test the loopback simulations."""
def test_two_chan_loopback(self, num_msgs):
to_hw = self.openEP(1001, sendType=self.schema.I1, recvType=self.schema.I32)
from_hw = self.openEP(1002,
to_hw = self.openEP(1002, sendType=self.schema.I1, recvType=self.schema.I32)
from_hw = self.openEP(1001,
sendType=self.schema.I32,
recvType=self.schema.I1)
for _ in range(num_msgs):

View File

@ -54,6 +54,17 @@ def ToClientOp : ESI_Op<"service.to_client",
}];
}
def ServiceDeclInOutOp : ESI_Op<"service.inout",
[HasParent<"::circt::esi::ServiceDeclOp">]> {
let summary = "An ESI service port which has both directions";
let arguments = (ins SymbolNameAttr:$inner_sym,
TypeAttr:$inType, TypeAttr:$outType);
let assemblyFormat = [{
$inner_sym attr-dict `:` qualified($inType) `->` qualified($outType)
}];
}
def ServiceInstanceOp : ESI_Op<"service.instance"> {
let summary = "Instantiate a server module";
let description = [{
@ -134,3 +145,18 @@ def RequestToClientConnectionOp : ESI_Op<"service.req.to_client", [
attr-dict `:` qualified(type($receiving))
}];
}
def RequestInOutChannelOp : ESI_Op<"service.req.inout", [
DeclareOpInterfaceMethods<SymbolUserOpInterface>]> {
let summary = "Request a bidirectional channel";
let arguments = (ins HWInnerRefAttr:$servicePort,
ChannelType:$sending,
StrArrayAttr:$clientNamePath);
let results = (outs ChannelType:$receiving);
let assemblyFormat = [{
$sending `->` $servicePort `(` $clientNamePath `)` attr-dict `:`
qualified(type($sending)) `->` qualified(type($receiving))
}];
}

View File

@ -20,8 +20,8 @@ class LoopbackTester(esi_cosim.CosimBase):
ep.close().wait()
def test_two_chan_loopback(self, num_msgs):
to_hw = self.openEP(11, sendType=self.schema.I1, recvType=self.schema.I8)
from_hw = self.openEP(12, sendType=self.schema.I8, recvType=self.schema.I1)
to_hw = self.openEP(12, sendType=self.schema.I1, recvType=self.schema.I8)
from_hw = self.openEP(11, sendType=self.schema.I8, recvType=self.schema.I1)
for _ in range(num_msgs):
data = random.randint(0, 2**8 - 1)
print(f"Sending {data}")

View File

@ -242,7 +242,6 @@ static OpType getServicePortDecl(Operation *op,
StringAttr modName = servicePort.getModule();
ServiceDeclOp serviceDeclOp = topSyms.lookup<ServiceDeclOp>(modName);
if (!serviceDeclOp) {
op->emitOpError("Cannot find module ") << modName;
return {};
}
@ -250,7 +249,6 @@ static OpType getServicePortDecl(Operation *op,
for (auto portDecl : serviceDeclOp.getOps<OpType>())
if (portDecl.inner_symAttr() == innerSym)
return portDecl;
op->emitOpError("Cannot find port named ") << innerSym;
return {};
}
@ -259,17 +257,34 @@ static OpType getServicePortDecl(Operation *op,
template <class PortTypeOp, class OpType>
static LogicalResult
reqPortMatches(OpType op, SymbolTableCollection &symbolTable, Type t) {
auto portDecl =
getServicePortDecl<PortTypeOp>(op, symbolTable, op.servicePort());
if (!portDecl)
return failure();
hw::InnerRefAttr port = op.servicePort();
auto portDecl = getServicePortDecl<PortTypeOp>(op, symbolTable, port);
auto inoutPortDecl =
getServicePortDecl<ServiceDeclInOutOp>(op, symbolTable, port);
if (!portDecl && !inoutPortDecl)
return op.emitOpError("Could not find service port declaration ")
<< port.getModuleRef() << "::" << port.getName().getValue();
auto *ctxt = op.getContext();
if (portDecl.type() != t &&
portDecl.type() != ChannelType::get(ctxt, AnyType::get(ctxt)))
return op.emitOpError("Request type does not match port type ")
<< portDecl.type();
auto anyChannelType = ChannelType::get(ctxt, AnyType::get(ctxt));
if (portDecl) {
if (portDecl.type() != t && portDecl.type() != anyChannelType)
return op.emitOpError("Request type does not match port type ")
<< portDecl.type();
return success();
}
assert(inoutPortDecl);
if (isa<ToClientOp>(op)) {
if (inoutPortDecl.inType() != t && inoutPortDecl.inType() != anyChannelType)
return op.emitOpError("Request type does not match port type ")
<< inoutPortDecl.inType();
} else if (isa<ToServerOp>(op)) {
if (inoutPortDecl.outType() != t &&
inoutPortDecl.outType() != anyChannelType)
return op.emitOpError("Request type does not match port type ")
<< inoutPortDecl.outType();
}
return success();
}
@ -283,5 +298,30 @@ LogicalResult RequestToServerConnectionOp::verifySymbolUses(
return reqPortMatches<ToServerOp>(*this, symbolTable, sending().getType());
}
LogicalResult
RequestInOutChannelOp::verifySymbolUses(SymbolTableCollection &symbolTable) {
auto portDecl = getServicePortDecl<ServiceDeclInOutOp>(
this->getOperation(), symbolTable, servicePort());
if (!portDecl)
return emitOpError("Could not find inout service port declaration.");
auto *ctxt = getContext();
auto anyChannelType = ChannelType::get(ctxt, AnyType::get(ctxt));
// Check the input port type.
if (portDecl.inType() != sending().getType() &&
portDecl.inType() != anyChannelType)
return emitOpError("Request to_server type does not match port type ")
<< portDecl.inType();
// Check the output port type.
if (portDecl.outType() != receiving().getType() &&
portDecl.outType() != anyChannelType)
return emitOpError("Request to_client type does not match port type ")
<< portDecl.outType();
return success();
}
#define GET_OP_CLASSES
#include "circt/Dialect/ESI/ESI.cpp.inc"

View File

@ -70,35 +70,87 @@ static LogicalResult instantiateCosimEndpointOps(ServiceImplementReqOp req) {
return StringAttr::get(ctxt, os.str());
};
// Create Cosim endpoints for the incoming data requests.
unsigned clientReqIdx = 0;
for (auto toClientReq :
llvm::make_early_inc_range(req.getOps<RequestToClientConnectionOp>())) {
auto cosimIn = b.create<NullSourceOp>(
toClientReq.getLoc(), ChannelType::get(ctxt, b.getI1Type()));
auto cosim = b.create<CosimEndpointOp>(toClientReq.getLoc(),
toClientReq.receiving().getType(),
clk, rst, cosimIn, ++epIdCtr);
cosim->setAttr("name", toStringAttr(toClientReq.clientNamePath()));
req.getResult(clientReqIdx).replaceAllUsesWith(cosim.recv());
toClientReq.erase();
++clientReqIdx;
}
// Since outgoing data gets passed in through block args, we need to translate
// internal Values (with the block) to the external Values driving them.
BlockAndValueMapping argMap;
for (unsigned i = 0, e = portReqs->getArguments().size(); i < e; ++i)
argMap.map(portReqs->getArgument(i), req->getOperand(i + 2));
// Create output Cosim endpoints.
// Build a mapping of client names to request.
DenseMap<ArrayAttr, SmallVector<Operation *, 0>> clientNameToOps;
for (auto &op : req.getOps())
if (auto req = dyn_cast<RequestToClientConnectionOp>(op))
clientNameToOps[req.clientNamePathAttr()].push_back(&op);
else if (auto req = dyn_cast<RequestToServerConnectionOp>(op))
clientNameToOps[req.clientNamePathAttr()].push_back(&op);
// For any client names with two requests of opposite directions, mark a
// paired.
DenseMap<RequestToServerConnectionOp, RequestToClientConnectionOp> pairs;
for (auto opsForClientName : clientNameToOps) {
const SmallVector<Operation *, 0> &ops = opsForClientName.second;
if (ops.size() != 2)
continue;
// Only proceed if a to_client and to_server are found.
RequestToClientConnectionOp to_client;
RequestToServerConnectionOp to_server;
if ((to_client = dyn_cast<RequestToClientConnectionOp>(ops[0]))) {
if (!(to_server = dyn_cast<RequestToServerConnectionOp>(ops[1])))
continue;
} else if ((to_server = dyn_cast<RequestToServerConnectionOp>(ops[0]))) {
if (!(to_client = dyn_cast<RequestToClientConnectionOp>(ops[1])))
continue;
}
assert(to_client);
assert(to_server);
pairs[to_server] = to_client;
}
// Create output Cosim endpoints. If the request is one half of a pair, store
// it.
DenseMap<RequestToClientConnectionOp, CosimEndpointOp> pairedCosimEndpoints;
for (auto toServerReq :
llvm::make_early_inc_range(req.getOps<RequestToServerConnectionOp>())) {
Type resType;
auto foundClient = pairs.find(toServerReq);
if (foundClient != pairs.end())
resType = foundClient->second.receiving().getType();
else
resType = ChannelType::get(ctxt, b.getI1Type());
auto cosim = b.create<CosimEndpointOp>(
toServerReq.getLoc(), ChannelType::get(ctxt, b.getI1Type()), clk, rst,
toServerReq.getLoc(), resType, clk, rst,
argMap.lookup(toServerReq.sending()), ++epIdCtr);
cosim->setAttr("name", toStringAttr(toServerReq.clientNamePath()));
toServerReq.erase();
if (foundClient != pairs.end())
pairedCosimEndpoints[foundClient->second] = cosim;
}
// Create Cosim endpoints for the incoming data requests.
unsigned clientReqIdx = 0;
for (auto toClientReq :
llvm::make_early_inc_range(req.getOps<RequestToClientConnectionOp>())) {
CosimEndpointOp cosim;
// If this is a paired request, use the paired endpoint.
auto epFound = pairedCosimEndpoints.find(toClientReq);
if (epFound != pairedCosimEndpoints.end()) {
cosim = epFound->second;
} else {
auto cosimIn = b.create<NullSourceOp>(
toClientReq.getLoc(), ChannelType::get(ctxt, b.getI1Type()));
cosim = b.create<CosimEndpointOp>(toClientReq.getLoc(),
toClientReq.receiving().getType(), clk,
rst, cosimIn, ++epIdCtr);
cosim->setAttr("name", toStringAttr(toClientReq.clientNamePath()));
}
req.getResult(clientReqIdx).replaceAllUsesWith(cosim.recv());
toClientReq.erase();
++clientReqIdx;
}
// Erase the generation request.
@ -202,6 +254,19 @@ LogicalResult ESIConnectServicesPass::process(hw::HWMutableModuleLike mod) {
for (auto instOp : modBlock.getOps<ServiceInstanceOp>())
localImplReqs[instOp.service_symbolAttr()] = new Block();
// Decompose the 'inout' requests int to 'in' and 'out' requests.
mod.walk([&](RequestInOutChannelOp reqInOut) {
ImplicitLocOpBuilder b(reqInOut.getLoc(), reqInOut);
b.create<RequestToServerConnectionOp>(reqInOut.servicePortAttr(),
reqInOut.sending(),
reqInOut.clientNamePathAttr());
auto toClientReq = b.create<RequestToClientConnectionOp>(
reqInOut.receiving().getType(), reqInOut.servicePortAttr(),
reqInOut.clientNamePathAttr());
reqInOut.receiving().replaceAllUsesWith(toClientReq.receiving());
reqInOut.erase();
});
// Find all of the "local" requests.
mod.walk([&](Operation *op) {
if (auto req = dyn_cast<RequestToClientConnectionOp>(op)) {

View File

@ -63,7 +63,7 @@ esi.service.decl @HostComms {
}
hw.module @Loopback (%clk: i1) -> () {
// expected-error @+1 {{'esi.service.req.to_client' op Cannot find port named "Recv"}}
// expected-error @+1 {{'esi.service.req.to_client' op Could not find service port declaration @HostComms::Recv}}
%dataIn = esi.service.req.to_client <@HostComms::@Recv> (["loopback_tohw"]) : !esi.channel<i32>
}
// -----
@ -79,8 +79,30 @@ hw.module @Loopback (%clk: i1) -> () {
// -----
esi.service.decl @HostComms {
esi.service.inout @ReqResp : !esi.channel<i8> -> !esi.channel<i16>
}
hw.module @Loopback (%clk: i1, %s: !esi.channel<i16>) -> () {
// expected-error @+1 {{'esi.service.req.inout' op Request to_server type does not match port type '!esi.channel<i8>'}}
%dataIn = esi.service.req.inout %s -> <@HostComms::@ReqResp> (["loopback_tohw"]) : !esi.channel<i16> -> !esi.channel<i16>
}
// -----
esi.service.decl @HostComms {
esi.service.inout @ReqResp : !esi.channel<i8> -> !esi.channel<i16>
}
hw.module @Loopback (%clk: i1, %s: !esi.channel<i8>) -> () {
// expected-error @+1 {{'esi.service.req.inout' op Request to_client type does not match port type '!esi.channel<i16>'}}
%dataIn = esi.service.req.inout %s -> <@HostComms::@ReqResp> (["loopback_tohw"]) : !esi.channel<i8> -> !esi.channel<i8>
}
// -----
hw.module @Loopback (%clk: i1) -> () {
// expected-error @+1 {{Cannot find module "HostComms"}}
// expected-error @+1 {{'esi.service.req.to_client' op Could not find service port declaration @HostComms::Recv}}
%dataIn = esi.service.req.to_client <@HostComms::@Recv> (["loopback_tohw"]) : !esi.channel<i32>
}

View File

@ -7,6 +7,7 @@
esi.service.decl @HostComms {
esi.service.to_server @Send : !esi.channel<!esi.any>
esi.service.to_client @Recv : !esi.channel<i8>
esi.service.inout @ReqResp : !esi.channel<i8> -> !esi.channel<i16>
}
// CHECK-LABEL: hw.module @Top(%clk: i1, %rst: i1) {
@ -14,9 +15,9 @@ esi.service.decl @HostComms {
// CHECK: hw.instance "m1" @Loopback(clk: %clk: i1) -> ()
// CONN-LABEL: hw.module @Top(%clk: i1, %rst: i1) {
// CONN: [[R2:%.+]] = esi.cosim %clk, %rst, %m1.loopback_fromhw, 21 {name = "m1.loopback_fromhw"} : !esi.channel<i8> -> !esi.channel<i1>
// CONN: [[R0:%.+]] = esi.null : !esi.channel<i1>
// CONN: [[R1:%.+]] = esi.cosim %clk, %rst, [[R0]], 21 {name = "m1.loopback_tohw"} : !esi.channel<i1> -> !esi.channel<i8>
// CONN: [[R2:%.+]] = esi.cosim %clk, %rst, %m1.loopback_fromhw, 22 {name = "m1.loopback_fromhw"} : !esi.channel<i8> -> !esi.channel<i1>
// CONN: [[R1:%.+]] = esi.cosim %clk, %rst, [[R0]], 22 {name = "m1.loopback_tohw"} : !esi.channel<i1> -> !esi.channel<i8>
// CONN: %m1.loopback_fromhw = hw.instance "m1" @Loopback(clk: %clk: i1, loopback_tohw: [[R1]]: !esi.channel<i8>) -> (loopback_fromhw: !esi.channel<i8>)
hw.module @Top (%clk: i1, %rst: i1) {
esi.service.instance @HostComms impl as "cosim" opts {EpID_start = 20} (%clk, %rst) : (i1, i1) -> ()
@ -108,3 +109,41 @@ msft.module @MsLoopback {} (%clk: i1) -> () {
esi.service.req.to_server %dataIn -> <@HostComms::@Send> (["loopback_fromhw"]) : !esi.channel<i8>
msft.output
}
// CONN-LABEL: msft.module @InOutTop {} (%clk: i1) -> (chksum: i8) {
// CONN: %0:2 = esi.service.impl_req @HostComms impl as "topComms"(%clk, %m1.loopback_inout) : (i1, !esi.channel<i8>) -> (i8, !esi.channel<i16>) {
// CONN: ^bb0(%arg0: !esi.channel<i8>):
// CONN: %1 = esi.service.req.to_client <@HostComms::@ReqResp>(["m1", "loopback_inout"]) : !esi.channel<i16>
// CONN: esi.service.req.to_server %arg0 -> <@HostComms::@ReqResp>(["m1", "loopback_inout"]) : !esi.channel<i8>
// CONN: }
// CONN: %m1.loopback_inout = msft.instance @m1 @InOutLoopback(%clk, %0#1) : (i1, !esi.channel<i16>) -> !esi.channel<i8>
// CONN: msft.output %0#0 : i8
msft.module @InOutTop {} (%clk: i1) -> (chksum: i8) {
%c = esi.service.instance @HostComms impl as "topComms" (%clk) : (i1) -> (i8)
msft.instance @m1 @InOutLoopback (%clk) : (i1) -> ()
msft.output %c : i8
}
// CONN-LABEL: msft.module @InOutLoopback {} (%clk: i1, %loopback_inout: !esi.channel<i16>) -> (loopback_inout: !esi.channel<i8>) {
// CONN: %rawOutput, %valid = esi.unwrap.vr %loopback_inout, %ready : i16
// CONN: %0 = comb.extract %rawOutput from 0 : (i16) -> i8
// CONN: %chanOutput, %ready = esi.wrap.vr %0, %valid : i8
// CONN: msft.output %chanOutput : !esi.channel<i8>
msft.module @InOutLoopback {} (%clk: i1) -> () {
%dataIn = esi.service.req.inout %dataTrunc -> <@HostComms::@ReqResp> (["loopback_inout"]) : !esi.channel<i8> -> !esi.channel<i16>
%unwrap, %valid = esi.unwrap.vr %dataIn, %rdy: i16
%trunc = comb.extract %unwrap from 0 : (i16) -> (i8)
%dataTrunc, %rdy = esi.wrap.vr %trunc, %valid : i8
msft.output
}
// CONN-LABEL: msft.module @LoopbackCosimTop {} (%clk: i1, %rst: i1) {
// CONN: [[R1:%.+]] = esi.cosim %clk, %rst, %m1.loopback_inout, 21 {name = "m1.loopback_inout"} : !esi.channel<i8> -> !esi.channel<i16>
// CONN: %m1.loopback_inout = msft.instance @m1 @InOutLoopback(%clk, %0) : (i1, !esi.channel<i16>) -> !esi.channel<i8>
msft.module @LoopbackCosimTop {} (%clk: i1, %rst: i1) {
esi.service.instance @HostComms impl as "cosim" opts {EpID_start = 20} (%clk, %rst) : (i1, i1) -> ()
msft.instance @m1 @InOutLoopback(%clk) : (i1) -> ()
msft.output
}