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

[ESI] Snoop op #8096

Merged
merged 1 commit into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions include/circt/Dialect/ESI/ESIChannels.td
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,52 @@ def ChannelTypeImpl : ESI_Type<"Channel"> {
::circt::esi::ChannelSignaling::ValidReady, 0);
}]>,
];

let extraClassDeclaration = [{
/// Consumers are ones which actually absorb tokens. Non-consumer ops
/// include any snooping operations.
static SmallVector<std::reference_wrapper<OpOperand>, 4> getConsumers(
mlir::TypedValue<ChannelType>);
static bool hasOneConsumer(mlir::TypedValue<ChannelType>);
static bool hasNoConsumers(mlir::TypedValue<ChannelType>);
static LogicalResult verifyChannel(mlir::TypedValue<ChannelType>);

/// Get the single consumer of a channel. Returns nullptr if there are zero
/// or more than one.
static OpOperand* getSingleConsumer(mlir::TypedValue<ChannelType>);
}];
}

//===----------------------------------------------------------------------===//
// Snoop operations reveal the internal signals of a channel.
//===----------------------------------------------------------------------===//

def SnoopValidReadyOp : ESI_Physical_Op<"snoop.vr", [InferTypeOpInterface]> {
let summary = "Get the valid, ready, and data signals from a channel";
let description = [{
A snoop allows one to combinationally observe a channel's internal signals.
It does not count as another user of the channel. Useful for constructing
control logic which can be combinationally driven. Also potentially useful
for debugging.
}];

let arguments = (ins ChannelType:$input);
let results = (outs I1:$valid, I1:$ready, AnyType:$data);
let hasVerifier = 1;
let assemblyFormat = [{
$input attr-dict `:` qualified(type($input))
}];

let extraClassDeclaration = [{
/// Infer the return types of this operation.
static LogicalResult inferReturnTypes(MLIRContext *context,
std::optional<Location> loc,
ValueRange operands,
DictionaryAttr attrs,
mlir::OpaqueProperties properties,
mlir::RegionRange regions,
SmallVectorImpl<Type> &results);
}];
}

//===----------------------------------------------------------------------===//
Expand Down
30 changes: 26 additions & 4 deletions lib/Dialect/ESI/ESIOps.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,30 @@ LogicalResult ChannelBufferOp::verify() {
return success();
}

//===----------------------------------------------------------------------===//
// Snoop operation functions.
//===----------------------------------------------------------------------===//

LogicalResult SnoopValidReadyOp::verify() {
ChannelType type = getInput().getType();
if (type.getSignaling() != ChannelSignaling::ValidReady)
return emitOpError("only supports valid-ready signaling");
if (type.getInner() != getData().getType())
return emitOpError("input and output types must match");
return success();
}

LogicalResult SnoopValidReadyOp::inferReturnTypes(
MLIRContext *context, std::optional<Location> loc, ValueRange operands,
DictionaryAttr attrs, mlir::OpaqueProperties properties,
mlir::RegionRange regions, SmallVectorImpl<Type> &results) {
auto i1 = IntegerType::get(context, 1);
results.push_back(i1);
results.push_back(i1);
results.push_back(cast<ChannelType>(operands[0].getType()).getInner());
return success();
}

//===----------------------------------------------------------------------===//
// FIFO functions.
//===----------------------------------------------------------------------===//
Expand Down Expand Up @@ -159,10 +183,8 @@ LogicalResult WrapValidReadyOp::verify() {
mlir::TypedValue<ChannelType> chanOut = getChanOutput();
if (chanOut.getType().getSignaling() != ChannelSignaling::ValidReady)
return emitOpError("only supports valid-ready signaling");
if (!chanOut.hasOneUse() && !chanOut.getUses().empty()) {
llvm::errs() << "chanOut: " << chanOut.getLoc() << "\n";
return emitOpError("only supports zero or one use");
}
if (failed(ChannelType::verifyChannel(chanOut)))
return failure();
return success();
}

Expand Down
43 changes: 43 additions & 0 deletions lib/Dialect/ESI/ESITypes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
//===----------------------------------------------------------------------===//

#include "circt/Dialect/ESI/ESITypes.h"
#include "circt/Dialect/ESI/ESIOps.h"
#include "circt/Dialect/HW/HWTypes.h"
#include "mlir/IR/Attributes.h"
#include "mlir/IR/DialectImplementation.h"
Expand All @@ -24,6 +25,48 @@ using namespace circt::esi;

AnyType AnyType::get(MLIRContext *context) { return Base::get(context); }

/// Get the list of users with snoops filtered out. Returns a filtered range
/// which is lazily constructed.
static auto getChannelConsumers(mlir::TypedValue<ChannelType> chan) {
return llvm::make_filter_range(chan.getUses(), [](auto &use) {
return !isa<SnoopValidReadyOp>(use.getOwner());
});
}
SmallVector<std::reference_wrapper<OpOperand>, 4>
ChannelType::getConsumers(mlir::TypedValue<ChannelType> chan) {
return SmallVector<std::reference_wrapper<OpOperand>, 4>(
getChannelConsumers(chan));
}
bool ChannelType::hasOneConsumer(mlir::TypedValue<ChannelType> chan) {
auto consumers = getChannelConsumers(chan);
if (consumers.empty())
return false;
return ++consumers.begin() == consumers.end();
}
bool ChannelType::hasNoConsumers(mlir::TypedValue<ChannelType> chan) {
return getChannelConsumers(chan).empty();
}
OpOperand *ChannelType::getSingleConsumer(mlir::TypedValue<ChannelType> chan) {
auto consumers = getChannelConsumers(chan);
auto iter = consumers.begin();
if (iter == consumers.end())
return nullptr;
OpOperand *result = &*iter;
if (++iter != consumers.end())
return nullptr;
return result;
}
LogicalResult ChannelType::verifyChannel(mlir::TypedValue<ChannelType> chan) {
auto consumers = getChannelConsumers(chan);
if (consumers.empty() || ++consumers.begin() == consumers.end())
return success();
auto err = chan.getDefiningOp()->emitOpError(
"channels must have at most one consumer");
for (auto &consumer : consumers)
err.attachNote(consumer.getOwner()->getLoc()) << "channel used here";
return err;
}

LogicalResult
WindowType::verify(llvm::function_ref<InFlightDiagnostic()> emitError,
StringAttr name, Type into,
Expand Down
20 changes: 16 additions & 4 deletions lib/Dialect/ESI/Passes/ESILowerToHW.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -159,19 +159,26 @@ struct RemoveWrapUnwrap : public ConversionPattern {
WrapValidReadyOp wrap = dyn_cast<WrapValidReadyOp>(op);
UnwrapValidReadyOp unwrap = dyn_cast<UnwrapValidReadyOp>(op);
if (wrap) {
if (wrap.getChanOutput().getUsers().empty()) {
// Lower away snoop ops.
for (auto user : wrap.getChanOutput().getUsers())
if (auto snoop = dyn_cast<SnoopValidReadyOp>(user))
rewriter.replaceOp(
snoop, {wrap.getValid(), wrap.getReady(), wrap.getRawInput()});

if (ChannelType::hasNoConsumers(wrap.getChanOutput())) {
auto c1 = rewriter.create<hw::ConstantOp>(wrap.getLoc(),
rewriter.getI1Type(), 1);
rewriter.replaceOp(wrap, {nullptr, c1});
return success();
}

if (!wrap.getChanOutput().hasOneUse())
if (!ChannelType::hasOneConsumer(wrap.getChanOutput()))
return rewriter.notifyMatchFailure(
wrap, "This conversion only supports wrap-unwrap back-to-back. "
"Wrap didn't have exactly one use.");
if (!(unwrap = dyn_cast<UnwrapValidReadyOp>(
wrap.getChanOutput().use_begin()->getOwner())))
ChannelType::getSingleConsumer(wrap.getChanOutput())
->getOwner())))
return rewriter.notifyMatchFailure(
wrap, "This conversion only supports wrap-unwrap back-to-back. "
"Could not find 'unwrap'.");
Expand All @@ -189,11 +196,16 @@ struct RemoveWrapUnwrap : public ConversionPattern {
valid = wrap.getValid();
data = wrap.getRawInput();
ready = operands[1];

// Lower away snoop ops.
for (auto user : operands[0].getUsers())
if (auto snoop = dyn_cast<SnoopValidReadyOp>(user))
rewriter.replaceOp(snoop, {valid, ready, data});
} else {
return failure();
}

if (!wrap.getChanOutput().hasOneUse())
if (!ChannelType::hasOneConsumer(wrap.getChanOutput()))
return rewriter.notifyMatchFailure(wrap, [](Diagnostic &d) {
d << "This conversion only supports wrap-unwrap back-to-back. "
"Wrap didn't have exactly one use.";
Expand Down
12 changes: 4 additions & 8 deletions lib/Dialect/ESI/Passes/ESIVerifyConnections.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//
//===----------------------------------------------------------------------===//

#include "circt/Dialect/ESI/ESIOps.h"
#include "circt/Dialect/ESI/ESIPasses.h"
#include "circt/Dialect/ESI/ESITypes.h"

Expand Down Expand Up @@ -42,14 +43,9 @@ void ESIVerifyConnectionsPass::runOnOperation() {
error.attachNote(user->getLoc()) << "bundle used here";
signalPassFailure();

} else if (isa<ChannelType>(v.getType())) {
if (std::distance(v.getUses().begin(), v.getUses().end()) <= 1)
continue;
mlir::InFlightDiagnostic error =
op->emitError("channels must have at most one use");
for (Operation *user : v.getUsers())
error.attachNote(user->getLoc()) << "channel used here";
signalPassFailure();
} else if (auto cv = dyn_cast<mlir::TypedValue<ChannelType>>(v)) {
if (failed(ChannelType::verifyChannel(cv)))
signalPassFailure();
}
});
}
Expand Down
8 changes: 6 additions & 2 deletions test/Dialect/ESI/connectivity.mlir
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// RUN: circt-opt %s -verify-diagnostics | circt-opt -verify-diagnostics | FileCheck %s
// RUN: circt-opt %s --verify-esi-connections

hw.module @Sender(out x: !esi.channel<i1>) {
%0 = arith.constant 0 : i1
Expand Down Expand Up @@ -38,8 +39,11 @@ hw.module @test(in %clk: !seq.clock, in %rst: i1) {
hw.instance "recv" @Reciever (a: %bufferedChan2: !esi.channel<i1>) -> ()

// CHECK-NEXT: %sender.x_0 = hw.instance "sender" @Sender() -> (x: !esi.channel<i1>)
// CHECK-NEXT: %1 = esi.buffer %clk, %rst, %sender.x_0 {stages = 4 : i64} : i1
// CHECK-NEXT: hw.instance "recv" @Reciever(a: %1: !esi.channel<i1>) -> ()
// CHECK-NEXT: [[R1:%.+]] = esi.buffer %clk, %rst, %sender.x_0 {stages = 4 : i64} : i1
// CHECK-NEXT: hw.instance "recv" @Reciever(a: [[R1]]: !esi.channel<i1>) -> ()

%valid, %ready, %data = esi.snoop.vr %bufferedChan2 : !esi.channel<i1>
// CHECK-NEXT: %valid, %ready, %data = esi.snoop.vr [[R1]] : !esi.channel<i1>

%nullBit = esi.null : !esi.channel<i1>
hw.instance "nullRcvr" @Reciever(a: %nullBit: !esi.channel<i1>) -> ()
Expand Down
6 changes: 4 additions & 2 deletions test/Dialect/ESI/errors.mlir
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ hw.module.extern @Source(out a: !esi.channel<i1>)
hw.module.extern @Sink(in %a: !esi.channel<i1>)

hw.module @Top() {
// expected-error @+1 {{channels must have at most one use}}
// expected-error @+1 {{channels must have at most one consumer}}
%a = hw.instance "src" @Source() -> (a: !esi.channel<i1>)
// expected-note @+1 {{channel used here}}
hw.instance "sink1" @Sink(a: %a: !esi.channel<i1>) -> ()
Expand Down Expand Up @@ -203,10 +203,12 @@ hw.module @Top() {
// -----

hw.module @wrap_multi_unwrap(in %a_data: i8, in %a_valid: i1, out a_ready: i1) {
// expected-error @+1 {{'esi.wrap.vr' op only supports zero or one use}}
// expected-error @+1 {{'esi.wrap.vr' op channels must have at most one consumer}}
%a_chan, %a_ready = esi.wrap.vr %a_data, %a_valid : i8
%true = hw.constant true
// expected-note @+1 {{channel used here}}
%ap_data, %ap_valid = esi.unwrap.vr %a_chan, %true : i8
// expected-note @+1 {{channel used here}}
%ab_data, %ab_valid = esi.unwrap.vr %a_chan, %true : i8
hw.output %a_ready : i1
}
23 changes: 14 additions & 9 deletions test/Dialect/ESI/lowering.mlir
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// RUN: circt-opt %s --lower-esi-to-physical -verify-diagnostics | circt-opt -verify-diagnostics | FileCheck %s
// RUN: circt-opt %s --lower-esi-ports -verify-diagnostics | circt-opt -verify-diagnostics | FileCheck --check-prefix=IFACE %s
// RUN: circt-opt %s --lower-esi-to-physical --lower-esi-ports --hw-flatten-io --lower-esi-to-hw -verify-diagnostics | circt-opt -verify-diagnostics | FileCheck --check-prefix=HW %s
// RUN: circt-opt %s --lower-esi-to-physical --lower-esi-ports --hw-flatten-io --lower-esi-to-hw | FileCheck --check-prefix=HW %s

hw.module.extern @Sender(in %clk: !seq.clock, out x: !esi.channel<i4>, out y: i8) attributes {esi.bundle}
hw.module.extern @ArrSender(out x: !esi.channel<!hw.array<4xi64>>) attributes {esi.bundle}
Expand Down Expand Up @@ -53,6 +53,7 @@ hw.module @test(in %clk: !seq.clock, in %rst:i1) {
// IFACE-NEXT: %[[#modport4:]] = sv.modport.get %i4ToRecv2 @source : !sv.interface<@IValidReady_i4> -> !sv.modport<@IValidReady_i4::@source>
// IFACE-NEXT: hw.instance "recv2" @Reciever(a: %[[#modport4:]]: !sv.modport<@IValidReady_i4::@source>, clk: %clk: !seq.clock) -> ()

// HW-LABEL: hw.module @test(in %clk : !seq.clock, in %rst : i1)
// After all 3 ESI lowering passes, there shouldn't be any ESI constructs!
// HW-NOT: esi
}
Expand All @@ -76,21 +77,25 @@ hw.module @InternRcvr(in %in: !esi.channel<!hw.array<4xi8>>) {}
hw.module @test2(in %clk: !seq.clock, in %rst:i1) {
%ints, %c4 = hw.instance "adder" @add11(clk: %clk: !seq.clock, ints: %ints: !esi.channel<i32>) -> (mutatedInts: !esi.channel<i32>, c4: i4)

%valid, %ready, %data = esi.snoop.vr %ints: !esi.channel<i32>
%xact = comb.and %valid, %ready : i1

%nullBit = esi.null : !esi.channel<i4>
hw.instance "nullRcvr" @Reciever(a: %nullBit: !esi.channel<i4>, clk: %clk: !seq.clock) -> ()

%nullArray = esi.null : !esi.channel<!hw.array<4xi8>>
hw.instance "nullInternRcvr" @InternRcvr(in: %nullArray: !esi.channel<!hw.array<4xi8>>) -> ()
}
// HW-LABEL: hw.module @test2(in %clk : !seq.clock, in %rst : i1) {
// HW: %adder.ints_ready, %adder.mutatedInts, %adder.mutatedInts_valid, %adder.c4 = hw.instance "adder" @add11(clk: %clk: !seq.clock, ints: %adder.mutatedInts: i32, ints_valid: %adder.mutatedInts_valid: i1, mutatedInts_ready: %adder.ints_ready: i1) -> (ints_ready: i1, mutatedInts: i32, mutatedInts_valid: i1, c4: i4)
// HW: [[ZERO:%.+]] = hw.bitcast %c0_i4 : (i4) -> i4
// HW: sv.interface.signal.assign %i4ToNullRcvr(@IValidReady_i4::@data) = [[ZERO]] : i4
// HW: [[ZM:%.+]] = sv.modport.get %{{.+}} @source : !sv.interface<@IValidReady_i4> -> !sv.modport<@IValidReady_i4::@source>
// HW: hw.instance "nullRcvr" @Reciever(a: [[ZM]]: !sv.modport<@IValidReady_i4::@source>, clk: %clk: !seq.clock) -> ()
// HW: %c0_i32 = hw.constant 0 : i32
// HW: [[ZA:%.+]] = hw.bitcast %c0_i32 : (i32) -> !hw.array<4xi8>
// HW: %nullInternRcvr.in_ready = hw.instance "nullInternRcvr" @InternRcvr(in: [[ZA]]: !hw.array<4xi8>, in_valid: %false_0: i1) -> (in_ready: i1)
// HW-NEXT: %adder.ints_ready, %adder.mutatedInts, %adder.mutatedInts_valid, %adder.c4 = hw.instance "adder" @add11(clk: %clk: !seq.clock, ints: %adder.mutatedInts: i32, ints_valid: %adder.mutatedInts_valid: i1, mutatedInts_ready: %adder.ints_ready: i1) -> (ints_ready: i1, mutatedInts: i32, mutatedInts_valid: i1, c4: i4)
// HW-NEXT: [[XACT:%.+]] = comb.and %adder.mutatedInts_valid, %adder.ints_ready : i1
// HW: [[ZERO:%.+]] = hw.bitcast %c0_i4 : (i4) -> i4
// HW: sv.interface.signal.assign %i4ToNullRcvr(@IValidReady_i4::@data) = [[ZERO]] : i4
// HW: [[ZM:%.+]] = sv.modport.get %{{.+}} @source : !sv.interface<@IValidReady_i4> -> !sv.modport<@IValidReady_i4::@source>
// HW: hw.instance "nullRcvr" @Reciever(a: [[ZM]]: !sv.modport<@IValidReady_i4::@source>, clk: %clk: !seq.clock) -> ()
// HW: %c0_i32 = hw.constant 0 : i32
// HW: [[ZA:%.+]] = hw.bitcast %c0_i32 : (i32) -> !hw.array<4xi8>
// HW: %nullInternRcvr.in_ready = hw.instance "nullInternRcvr" @InternRcvr(in: [[ZA]]: !hw.array<4xi8>, in_valid: %false_0: i1) -> (in_ready: i1)

hw.module @twoChannelArgs(in %clk: !seq.clock, in %ints: !esi.channel<i32>, in %foo: !esi.channel<i7>) {
%rdy = hw.constant 1 : i1
Expand Down
Loading