[ESI] Wrap modules exposing latency insensitive signals to expose ESI channels (#828)

Add two functions which are intended to be exposed over an API:
- A function to heuristically locate signal port triplets with ready valid on an RTL module.
- A function which takes an RTL module and a list of those port triplets and build a 'shell' around that module which 'uplifts' the port triplets to ESI channels.
Adds an 'esi-tester' binary to execute these two functions.
This commit is contained in:
John Demme 2021-03-27 14:06:19 -07:00 committed by GitHub
parent 4fdba7023e
commit 31d19839f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 380 additions and 4 deletions

View File

@ -19,8 +19,11 @@
#ifndef CIRCT_DIALECT_ESI_ESIDIALECT_H
#define CIRCT_DIALECT_ESI_ESIDIALECT_H
#include "circt/Dialect/RTL/RTLOps.h"
#include "circt/Support/LLVM.h"
#include "mlir/IR/BuiltinAttributes.h"
#include "mlir/IR/Dialect.h"
namespace circt {
namespace esi {
@ -46,6 +49,23 @@ private:
void registerESIPasses();
void registerESITranslations();
/// A triple of signals which represent a latency insensitive interface with
/// valid/ready semantics.
struct ESIPortValidReadyMapping {
rtl::ModulePortInfo data, valid, ready;
};
/// Find all the port triples on a module which fit the
/// <name>/<name>_valid/<name>_ready pattern. Ready must be the opposite
/// direction of the other two.
void findValidReadySignals(Operation *modOp,
SmallVectorImpl<ESIPortValidReadyMapping> &names);
/// Build an ESI module wrapper, converting the wires with latency-insensitive
/// semantics to ESI channels and passing through the rest.
Operation *buildESIWrapper(OpBuilder &b, Operation *mod,
ArrayRef<ESIPortValidReadyMapping> esiPortNames);
} // namespace esi
} // namespace circt

View File

@ -13,12 +13,19 @@
#include "circt/Dialect/ESI/ESIDialect.h"
#include "circt/Dialect/ESI/ESIOps.h"
#include "circt/Dialect/ESI/ESITypes.h"
#include "circt/Dialect/RTL/RTLOps.h"
#include "circt/Support/BackedgeBuilder.h"
#include "circt/Support/ImplicitLocOpBuilder.h"
#include "circt/Support/LLVM.h"
#include "mlir/IR/Builders.h"
#include "mlir/IR/DialectImplementation.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/ADT/StringSet.h"
#include "llvm/Support/FormatVariadic.h"
namespace circt {
namespace esi {
using namespace circt;
using namespace circt::esi;
ESIDialect::ESIDialect(MLIRContext *context)
: Dialect("esi", context, TypeID::get<ESIDialect>()) {
@ -30,7 +37,238 @@ ESIDialect::ESIDialect(MLIRContext *context)
#include "circt/Dialect/ESI/ESI.cpp.inc"
>();
}
} // namespace esi
} // namespace circt
/// Find all the port triples on a module which fit the
/// <name>/<name>_valid/<name>_ready pattern. Ready must be the opposite
/// direction of the other two.
void circt::esi::findValidReadySignals(
Operation *modOp, SmallVectorImpl<ESIPortValidReadyMapping> &names) {
SmallVector<rtl::ModulePortInfo, 64> ports;
rtl::getModulePortInfo(modOp, ports);
llvm::StringMap<rtl::ModulePortInfo> nameMap(ports.size());
for (auto port : ports)
nameMap[port.getName()] = port;
for (auto port : ports) {
if (port.direction == rtl::PortDirection::INOUT)
continue;
StringRef portDataName = port.getName();
if (portDataName.endswith("_data")) // Detect both `foo` and `foo_data`.
portDataName = portDataName.substr(0, portDataName.size() - 5);
// Try to find a corresponding 'valid' port.
SmallString<64> portName = portDataName;
portName.append("_valid");
auto valid = nameMap.find(portName);
if (valid == nameMap.end() || valid->second.direction != port.direction ||
!valid->second.type.isSignlessInteger(1))
continue;
// Try to find a corresponding 'ready' port.
portName = portDataName;
portName.append("_ready");
rtl::PortDirection readyDir = port.direction == rtl::PortDirection::INPUT
? rtl::PortDirection::OUTPUT
: rtl::PortDirection::INPUT;
auto ready = nameMap.find(portName);
if (ready == nameMap.end() || ready->second.direction != readyDir ||
!valid->second.type.isSignlessInteger(1))
continue;
// Found one.
names.push_back(ESIPortValidReadyMapping{
.data = port, .valid = valid->second, .ready = ready->second});
}
}
/// Build an ESI module wrapper, converting the wires with latency-insensitive
/// semantics to ESI channels and passing through the rest.
Operation *
circt::esi::buildESIWrapper(OpBuilder &b, Operation *pearl,
ArrayRef<ESIPortValidReadyMapping> portsToConvert) {
// In order to avoid the similar sounding and looking "wrapped" and "wrapper"
// names or the ambiguous "module", we use "pearl" for the module _being
// wrapped_ and "shell" for the _wrapper_ modules which is being created
// (terms typically used in latency insensitive design papers).
auto *ctxt = b.getContext();
Location loc = pearl->getLoc();
FunctionType modType = rtl::getModuleType(pearl);
SmallVector<rtl::ModulePortInfo, 64> pearlPorts;
rtl::getModulePortInfo(pearl, pearlPorts);
// -----
// First, build up a set of data structures to use throughout this function.
StringSet<> controlPorts; // Memoize the list of ready/valid ports to ignore.
llvm::StringMap<ESIPortValidReadyMapping>
dataPortMap; // Store a lookup table of ports to convert indexed on the
// data port name.
// Validate input and assemble lookup structures.
for (const auto &esiPort : portsToConvert) {
if (esiPort.data.direction == rtl::PortDirection::INOUT) {
pearl->emitError("Data signal '")
<< esiPort.data.name << "' must not be INOUT";
return nullptr;
}
dataPortMap[esiPort.data.name.getValue()] = esiPort;
if (esiPort.valid.direction != esiPort.data.direction) {
pearl->emitError("Valid port '")
<< esiPort.valid.name << "' direction must match data port.";
return nullptr;
}
if (esiPort.valid.type != b.getI1Type()) {
pearl->emitError("Valid signal '")
<< esiPort.valid.name << "' must be i1 type";
return nullptr;
}
controlPorts.insert(esiPort.valid.name.getValue());
if (esiPort.ready.direction != (esiPort.data.isOutput()
? rtl::PortDirection::INPUT
: rtl::PortDirection::OUTPUT)) {
pearl->emitError("Ready port '")
<< esiPort.ready.name
<< "' must be opposite direction to data signal.";
return nullptr;
}
if (esiPort.ready.type != b.getI1Type()) {
pearl->emitError("Ready signal '")
<< esiPort.ready.name << "' must be i1 type";
return nullptr;
}
controlPorts.insert(esiPort.ready.name.getValue());
}
// -----
// Second, build a list of ports for the shell module, skipping the
// valid/ready, and converting the ESI data ports to the ESI channel port
// type. Store some bookkeeping information.
SmallVector<rtl::ModulePortInfo, 64> shellPorts;
// Map the shell operand to the pearl port.
SmallVector<rtl::ModulePortInfo, 64> inputPortMap;
// Map the shell result to the pearl port.
SmallVector<rtl::ModulePortInfo, 64> outputPortMap;
for (const auto port : pearlPorts) {
if (controlPorts.contains(port.name.getValue()))
continue;
rtl::ModulePortInfo newPort = port;
if (dataPortMap.find(port.name.getValue()) != dataPortMap.end())
newPort.type = esi::ChannelPort::get(ctxt, port.type);
if (port.isOutput()) {
newPort.argNum = outputPortMap.size();
outputPortMap.push_back(port);
} else {
newPort.argNum = inputPortMap.size();
inputPortMap.push_back(port);
}
shellPorts.push_back(newPort);
}
// -----
// Third, create the shell module and also some builders for the inside.
SmallString<64> shellNameBuf;
StringAttr shellName = b.getStringAttr(
(SymbolTable::getSymbolName(pearl) + "_esi").toStringRef(shellNameBuf));
auto shell = b.create<rtl::RTLModuleOp>(loc, shellName, shellPorts);
shell.getBodyBlock()->clear(); // Erase the terminator.
auto modBuilder =
ImplicitLocOpBuilder::atBlockBegin(loc, shell.getBodyBlock());
BackedgeBuilder bb(modBuilder, modBuilder.getLoc());
// Hold the operands for `rtl.output` here.
SmallVector<Value, 64> outputs(shell.getNumResults());
// -----
// Fourth, assemble the inputs for the pearl module AND build all the ESI wrap
// and unwrap ops for both the input and output channels.
SmallVector<Value, 64> pearlOperands(modType.getNumInputs());
// Since we build all the ESI wrap and unwrap operations before pearl
// instantiation, we only need backedges from the pearl instance result. Index
// the backedges by the pearl modules result number.
llvm::DenseMap<size_t, Backedge> backedges;
// Go through the shell input ports, either tunneling them through or
// unwrapping the ESI channels. We'll need backedges for the ready signals
// since they are results from the upcoming pearl instance.
for (const auto port : shellPorts) {
if (port.isOutput())
continue;
Value arg = shell.getArgument(port.argNum);
auto esiPort = dataPortMap.find(port.name.getValue());
if (esiPort == dataPortMap.end()) {
// If it's just a regular port, it just gets passed through.
size_t pearlOpNum = inputPortMap[port.argNum].argNum;
pearlOperands[pearlOpNum] = arg;
continue;
}
Backedge ready = bb.get(modBuilder.getI1Type());
backedges.insert(std::make_pair(esiPort->second.ready.argNum, ready));
auto unwrap = modBuilder.create<UnwrapValidReady>(arg, ready);
pearlOperands[esiPort->second.data.argNum] = unwrap.rawOutput();
pearlOperands[esiPort->second.valid.argNum] = unwrap.valid();
}
// Iterate through the shell output ports, identify the ESI channels, and
// build ESI wrapper ops for signals being output from the pearl. The data and
// valid for these wrap ops will need to be backedges.
for (const auto port : shellPorts) {
if (!port.isOutput())
continue;
auto esiPort = dataPortMap.find(port.name.getValue());
if (esiPort == dataPortMap.end())
continue;
Backedge data = bb.get(esiPort->second.data.type);
Backedge valid = bb.get(modBuilder.getI1Type());
auto wrap = modBuilder.create<WrapValidReady>(data, valid);
backedges.insert(std::make_pair(esiPort->second.data.argNum, data));
backedges.insert(std::make_pair(esiPort->second.valid.argNum, valid));
outputs[port.argNum] = wrap.chanOutput();
pearlOperands[esiPort->second.ready.argNum] = wrap.ready();
}
// -----
// Fifth, instantiate the pearl module.
auto pearlInst = modBuilder.create<rtl::InstanceOp>(
modType.getResults(), "pearl", SymbolTable::getSymbolName(pearl),
pearlOperands, DictionaryAttr());
// Hookup all the backedges.
for (size_t i = 0, e = pearlInst.getNumResults(); i < e; ++i) {
auto backedge = backedges.find(i);
if (backedge != backedges.end())
backedge->second.setValue(pearlInst.getResult(i));
}
// -----
// Finally, find all the regular outputs and either tunnel them through.
for (const auto port : shellPorts) {
if (!port.isOutput())
continue;
auto esiPort = dataPortMap.find(port.name.getValue());
if (esiPort != dataPortMap.end())
continue;
size_t pearlResNum = outputPortMap[port.argNum].argNum;
outputs[port.argNum] = pearlInst.getResult(pearlResNum);
}
modBuilder.create<rtl::OutputOp>(outputs);
return shell;
}
#include "circt/Dialect/ESI/ESIAttrs.cpp.inc"

View File

@ -1,4 +1,5 @@
add_subdirectory(CAPI)
add_subdirectory(Dialect)
configure_lit_site_cfg(
${CMAKE_CURRENT_SOURCE_DIR}/lit.site.cfg.py.in
@ -12,6 +13,7 @@ set(CIRCT_TEST_DEPENDS
circt-capi-ir-test
circt-opt
circt-translate
esi-tester
handshake-runner
firtool
llhd-sim

View File

@ -0,0 +1,9 @@
##===- CMakeLists.txt - ---------------------------------------*- cmake -*-===//
##
## 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
##
##===----------------------------------------------------------------------===//
add_subdirectory(ESI)

View File

@ -0,0 +1,30 @@
##===- CMakeLists.txt - ---------------------------------------*- cmake -*-===//
##
## 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
##
##===----------------------------------------------------------------------===//
set(LLVM_LINK_COMPONENTS
Support
)
add_llvm_tool(esi-tester
esi-tester.cpp
)
llvm_update_compile_flags(esi-tester)
target_link_libraries(esi-tester
PRIVATE
CIRCTESI
CIRCTRTL
CIRCTSV
MLIRParser
MLIRSupport
MLIRIR
MLIROptLib
MLIRStandard
MLIRTransforms
MLIRLLVMIR
)

View File

@ -0,0 +1,52 @@
//===- esi-tester.cpp - The ESI test driver -------------------------------===//
//
// 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 program exercises some ESI functionality which is intended to be for API
// use only.
//
//===----------------------------------------------------------------------===//
#include "circt/InitAllDialects.h"
#include "circt/InitAllPasses.h"
#include "mlir/Dialect/StandardOps/IR/Ops.h"
#include "mlir/Pass/Pass.h"
#include "mlir/Support/MlirOptMain.h"
#include "mlir/Transforms/Passes.h"
using namespace circt;
using namespace circt::esi;
/// This is a test pass for verifying FuncOp's eraseResult method.
struct TestESIModWrap
: public mlir::PassWrapper<TestESIModWrap, OperationPass<mlir::ModuleOp>> {
void runOnOperation() override {
auto mlirMod = getOperation();
auto b = mlir::OpBuilder::atBlockEnd(mlirMod.getBody());
SmallVector<rtl::RTLModuleOp, 8> mods;
for (Operation *mod : mlirMod.getOps<rtl::RTLModuleExternOp>()) {
SmallVector<ESIPortValidReadyMapping, 32> liPorts;
findValidReadySignals(mod, liPorts);
if (!liPorts.empty())
if (!buildESIWrapper(b, mod, liPorts))
signalPassFailure();
}
}
};
int main(int argc, char **argv) {
mlir::DialectRegistry registry;
registry.insert<comb::CombDialect, esi::ESIDialect, rtl::RTLDialect>();
mlir::PassRegistration<TestESIModWrap>(
"test-mod-wrap", "Test the ESI find and wrap functionality");
return mlir::failed(
mlir::MlirOptMain(argc, argv, "CIRCT modular optimizer driver", registry,
/*preloadDialectsInContext=*/true));
}

View File

@ -0,0 +1,24 @@
// RUN: esi-tester %s --test-mod-wrap | FileCheck %s
rtl.module.extern @OutputChannel(%clk: i1, %bar_ready: i1) -> (%bar: i42, %bar_valid: i1)
// CHECK-LABEL: rtl.module @OutputChannel_esi(%clk: i1) -> (%bar: !esi.channel<i42>) {
// CHECK: %chanOutput, %ready = esi.wrap.vr %pearl.bar, %pearl.bar_valid : i42
// CHECK: %pearl.bar, %pearl.bar_valid = rtl.instance "pearl" @OutputChannel(%clk, %ready) : (i1, i1) -> (i42, i1)
// CHECK: rtl.output %chanOutput : !esi.channel<i42>
rtl.module.extern @InputChannel(%clk: i1, %foo_data: i23, %foo_valid: i1) -> (%foo_ready: i1, %rawOut: i99)
// CHECK-LABEL: rtl.module @InputChannel_esi(%clk: i1, %foo_data: !esi.channel<i23>) -> (%rawOut: i99) {
// CHECK: %rawOutput, %valid = esi.unwrap.vr %foo_data, %pearl.foo_ready : i23
// CHECK: %pearl.foo_ready, %pearl.rawOut = rtl.instance "pearl" @InputChannel(%clk, %rawOutput, %valid) : (i1, i23, i1) -> (i1, i99)
// CHECK: rtl.output %pearl.rawOut : i99
rtl.module.extern @Mixed(%clk: i1, %foo: i23, %foo_valid: i1, %bar_ready: i1) ->
(%bar: i42, %bar_valid: i1, %foo_ready: i1, %rawOut: i99)
// CHECK-LABEL: rtl.module @Mixed_esi(%clk: i1, %foo: !esi.channel<i23>) -> (%bar: !esi.channel<i42>, %rawOut: i99) {
// CHECK: %rawOutput, %valid = esi.unwrap.vr %foo, %pearl.foo_ready : i23
// CHECK: %chanOutput, %ready = esi.wrap.vr %pearl.bar, %pearl.bar_valid : i42
// CHECK: %pearl.bar, %pearl.bar_valid, %pearl.foo_ready, %pearl.rawOut = rtl.instance "pearl" @Mixed(%clk, %rawOutput, %valid, %ready) : (i1, i23, i1, i1) -> (i42, i1, i1, i99)
// CHECK: rtl.output %chanOutput, %pearl.rawOut : !esi.channel<i42>, i99

View File

@ -61,6 +61,7 @@ tools = [
'circt-opt',
'circt-translate',
'circt-capi-ir-test',
'esi-tester',
'llhd-sim'
]