[CalyxToFSM] Add FSM-flow remove groups pass (#4600)

This commit adds a version of the Calyx remove-groups pass tailored for
the FSM flow. Specifically, it will perform FSM outlining alongside group
rewriting - which interacts with the FSM I/O (group go/done signals).

Technically, we now (finally) have an end-to-end flow for SCF->Calyx->FSM->SV.
The usability of the end-to-end flow is still fairly restricted, mainly
due to:
1. the lack of support for lowering Calyx memory ops
2. The lack of support for lowering `calyx.par` ops in the FSM lowering
   (this shouldn't be too difficult).

A fair amount of state is stored in attributes in between `materialize-calyx-to-fsm`
and `calyx-remove-groups-fsm`. I'm debating with myself whether that's code
smell or just a necessary evil given the approach taken. Nevertheless, I
did this work because of being frustrated with the disconnection of Calyx
from the rest of CIRCT, which renders it unuseable. Since I was the author
of the FSM flow, I figured that i'd might aswell finish the end-to-end
prototype before stepping somewhat away from Calyx-related development.

I'm hoping that given this end-to-end prototype, there's enough existing
infrastructure for others to pick up the torch and flesh out the
missing pieces.

Some additional notes:
* Also makes `calyx.component` contain a graph region to make it a bit less
of a pain to specify SSA dependencies between cells.
* Also fixes various small bugs highlighted by this conversion.
This commit is contained in:
Morten Borup Petersen 2023-01-31 14:52:24 +01:00 committed by GitHub
parent 47c0eade67
commit e80520b7c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 726 additions and 88 deletions

View File

@ -29,10 +29,21 @@ namespace calyxToFSM {
// Entry and exit state names of the Calyx control program in the FSM.
static constexpr std::string_view sEntryStateName = "fsm_entry";
static constexpr std::string_view sExitStateName = "fsm_exit";
static constexpr std::string_view sGroupDoneInputs =
"calyx.fsm_group_done_inputs";
static constexpr std::string_view sGroupGoOutputs =
"calyx.fsm_group_go_outputs";
static constexpr std::string_view sSSAInputIndices = "calyx.fsm_ssa_inputs";
static constexpr std::string_view sFSMTopLevelGoIndex =
"calyx.fsm_top_level_go";
static constexpr std::string_view sFSMTopLevelDoneIndex =
"calyx.fsm_top_level_done";
} // namespace calyxToFSM
std::unique_ptr<mlir::Pass> createCalyxToFSMPass();
std::unique_ptr<mlir::Pass> createMaterializeCalyxToFSMPass();
std::unique_ptr<mlir::Pass> createRemoveGroupsFromFSMPass();
} // namespace circt

View File

@ -264,6 +264,76 @@ def MaterializeCalyxToFSM : Pass<"materialize-calyx-to-fsm", "calyx::ComponentOp
}
def CalyxRemoveGroupsFromFSM : Pass<"calyx-remove-groups-fsm", "calyx::ComponentOp"> {
let summary = "Perform FSM outlining and group removal";
let description = [{
This pass will outline the FSM into module scope and replace any SSA value references
from within the FSM body with additional inputs. Given this, the FSM
is instantiated as a `fsm.hw_module` operation within the Calyx component.
Using the FSM I/O (which is the group go/done signals), the `calyx.group`
operations are removed from the component, with the group go and done signals
being wired up to the FSM instance.
Example:
```mlir
calyx.component {
%reg, ... = calyx.register ... : i1
calyx.wires {
// Groups have explicit done signals, and assignments are not guarded
// by a group go signal.
calyx.group @A {
...
calyx.assign %reg = ...
...
calyx.group_done %foo ? %bar
}
}
calyx.control {
// Machine is defined inside the `calyx.control` operation and references
// SSA values defined outside the machine.
fsm.machine @control(%A_done : i1) -> (%A_go : i1) {
...
%0 = comb.not %reg // reference some SSA value defined outside the machine
...
}
}
}
```
into
```mlir
// The machine has been outlined into module scope, and no longer references
// any SSA values defined outside the machine. It is now fully independent
// from any notion of Calyx.
fsm.machine @control(%A_done : i1, %reg : i1) -> (%A_go : i1) {
...
%0 = comb.not %reg // reference some SSA value defined outside the machine
...
}
calyx.component {
%reg, ... = calyx.register ...
// Done signals are now wires
%A_done_in, %A_done_out = calyx.wire : i1
// The FSM is now instantiated as an `fsm.hwinstance` module
%A_go = fsm.hwinstance @control(%A_done_out, %reg) : ...
calyx.wires {
// Groups have been inlined, the group go signal is now a guard for
// all assignments, and `calyx.group_done` operations have been
// replaced by wire assignments.
...
calyx.assign %reg = %A_go ? ...
...
calyx.assign %A_done_in = %foo ? %bar
}
calyx.control {
}
}
```
}];
let dependentDialects = ["fsm::FSMDialect", "comb::CombDialect", "hw::HWDialect"];
let constructor = "circt::createRemoveGroupsFromFSMPass()";
}
//===----------------------------------------------------------------------===//
// FSMToSV
//===----------------------------------------------------------------------===//

View File

@ -14,6 +14,7 @@
#define CIRCT_DIALECT_CALYX_CALYXHELPERS_H
#include "circt/Dialect/Calyx/CalyxOps.h"
#include "circt/Dialect/Comb/CombOps.h"
#include "circt/Dialect/HW/HWOps.h"
#include "circt/Support/LLVM.h"
@ -50,6 +51,22 @@ void addMandatoryComponentPorts(PatternRewriter &rewriter,
// than zero. See: https://github.com/llvm/circt/issues/2660
unsigned handleZeroWidth(int64_t dim);
/// Updates the guard of each assignment within a group with `op`.
template <typename Op>
static void updateGroupAssignmentGuards(OpBuilder &builder, GroupOp &group,
Op &op) {
group.walk([&](AssignOp assign) {
if (assign.getGuard())
// If the assignment is guarded already, take the bitwise & of the current
// guard and the group's go signal.
assign->setOperand(2, builder.create<comb::AndOp>(
group.getLoc(), assign.getGuard(), op, false));
else
// Otherwise, just insert it as the guard.
assign->insertOperands(2, {op});
});
}
} // namespace calyx
} // namespace circt

View File

@ -218,6 +218,14 @@ def SliceLibOp : UnaryLibraryOp<"slice"> {
def NotLibOp : UnaryLibraryOp<"not"> {}
def WireLibOp : UnaryLibraryOp<"wire", [SameTypeConstraint<"in", "out">]> {}
def WireLibOp : UnaryLibraryOp<"wire", [SameTypeConstraint<"in", "out">]> {
let builders = [
OpBuilder<(ins "StringRef":$sym_name, "::mlir::Type":$wireType), [{
llvm::SmallVector<mlir::Type> resultTypes = {wireType, wireType};
$_state.addAttribute(mlir::SymbolTable::getSymbolAttrName(), $_builder.getStringAttr(sym_name));
$_state.addTypes(resultTypes);
}]>
];
}
def ExtSILibOp : UnaryLibraryOp<"extsi"> {}

View File

@ -39,6 +39,7 @@ def UndefinedOp : CalyxOp<"undef", [
def ComponentOp : CalyxOp<"component", [
HasParent<"mlir::ModuleOp">,
RegionKindInterface,
SymbolTable, /* contains Cell names. */
Symbol,
FunctionOpInterface,
@ -85,6 +86,9 @@ def ComponentOp : CalyxOp<"component", [
using mlir::detail::FunctionOpInterfaceTrait<ComponentOp>::front;
using mlir::detail::FunctionOpInterfaceTrait<ComponentOp>::getFunctionBody;
// Implement RegionKindInterface.
static RegionKind getRegionKind(unsigned index) { return RegionKind::Graph;}
/// Returns the argument types of this function.
ArrayRef<Type> getArgumentTypes() { return getFunctionType().getInputs(); }

View File

@ -3,18 +3,15 @@
// RUN: circt-opt %s \
// RUN: --lower-scf-to-calyx -canonicalize \
// RUN: --calyx-remove-comb-groups --canonicalize \
// RUN: --calyx-go-insertion --canonicalize \
// RUN: --lower-calyx-to-fsm --canonicalize \
// RUN: --materialize-calyx-to-fsm
// RUN: --materialize-calyx-to-fsm --canonicalize \
// RUN: --calyx-remove-groups-fsm --canonicalize | FileCheck %s
// This is the end of the road (for now) for Calyx in CIRCT.
// The materialized FSM now needs to be outlined from within the
// calyx module, and within the Calyx module it can be instantiated
// as any other HW component. The FSM will then be lowered through
// the existing FSM-to-HW flow.
// This is the end of the road for this example since there (as of writing)
// does not yet exist a lowering for calyx.memory operations.
// CHECK: calyx.control {
// CHECK: fsm.machine @control(
// CHECK: fsm.machine @control
// CHECK: calyx.component @main
func.func @main() {
%c0 = arith.constant 0 : index

View File

@ -0,0 +1,26 @@
// This test lowers an SCF construct through Calyx, FSM and (TODO)
// to RTL.
// RUN: circt-opt %s \
// RUN: --lower-scf-to-calyx -canonicalize \
// RUN: --calyx-remove-comb-groups --canonicalize \
// RUN: --lower-calyx-to-fsm --canonicalize \
// RUN: --materialize-calyx-to-fsm --canonicalize \
// RUN: --calyx-remove-groups-fsm --canonicalize \
// RUN: --lower-calyx-to-hw --canonicalize \
// RUN: --convert-fsm-to-sv | FileCheck %s
// TODO: ... simulate the hardware!
// CHECK: hw.module @control
// CHECK: hw.module @main
// CHECK: hw.instance "controller" @control
func.func @main(%arg0 : i32, %arg1 : i32) -> i32 {
%0 = arith.cmpi slt, %arg0, %arg1 : i32
cf.cond_br %0, ^bb1, ^bb2
^bb1:
cf.br ^bb3(%arg0 : i32)
^bb2:
cf.br ^bb3(%arg1 : i32)
^bb3(%1 : i32):
return %1 : i32
}

View File

@ -1,6 +1,7 @@
add_circt_library(CIRCTCalyxToFSM
CalyxToFSM.cpp
MaterializeFSM.cpp
RemoveGroupsFromFSM.cpp
ADDITIONAL_HEADER_DIRS
${MLIR_MAIN_INCLUDE_DIR}/mlir/Conversion/CalyxToFSM

View File

@ -63,6 +63,9 @@ struct MaterializeCalyxToFSMPass
for (auto transition :
stateOp.getTransitions().getOps<fsm::TransitionOp>()) {
if (!transition.hasGuard() && doneGuards.empty())
continue;
transition.ensureGuard(b);
auto guardOp = transition.getGuardReturn();
llvm::SmallVector<Value> guards;
@ -110,14 +113,103 @@ struct MaterializeCalyxToFSMPass
/// Constant cache.
DenseMap<APInt, Value> constants;
OpBuilder *b;
FSMStateNode *entryState;
FSMStateNode *exitState;
// Walks the machine and gathers the set of referenced groups and SSA values.
void walkMachine();
// Creates the top-level group go/done I/O for the machine.
void materializeGroupIO();
// Add attributes to the machine op to indicate which in/out ports are
// associated with group activations and which are additional inputs to the
// FSM.
void assignAttributes();
};
} // end anonymous namespace
void MaterializeCalyxToFSMPass::walkMachine() {
// Walk the states of the machine and gather the relation between states and
// the groups which they enable as well as the set of all enabled states.
for (auto stateOp : machineOp.getOps<fsm::StateOp>()) {
for (auto enableOp : llvm::make_early_inc_range(
stateOp.getOutput().getOps<calyx::EnableOp>())) {
auto groupName = enableOp.getGroupNameAttr().getAttr();
stateEnables[stateOp].insert(groupName);
referencedGroups.insert(groupName);
// Erase the enable op now that we've recorded the information.
enableOp.erase();
}
}
}
void MaterializeCalyxToFSMPass::materializeGroupIO() {
// Materialize the top-level I/O ports of the fsm.machine. We add an in- and
// output for every unique group referenced within the machine, as well as an
// additional in- and output to represent the top-level "go" input and "done"
// output ports.
SmallVector<Type> ioTypes = SmallVector<Type>(
referencedGroups.size() + /*top-level go/done*/ 1, b->getI1Type());
size_t nGroups = ioTypes.size() - 1;
machineOp.setType(b->getFunctionType(ioTypes, ioTypes));
assert(machineOp.getBody().getNumArguments() == 0 &&
"expected no inputs to the FSM");
machineOp.getBody().addArguments(
ioTypes, SmallVector<Location, 4>(ioTypes.size(), b->getUnknownLoc()));
// Build output assignments and transition guards in every state. We here
// assume that the ordering of states in referencedGroups is fixed and
// deterministic, since it is used as an analogue for port I/O ordering.
for (auto stateOp : machineOp.getOps<fsm::StateOp>()) {
assignStateOutputOperands(*b, stateOp,
/*topLevelDone=*/false);
assignStateTransitionGuard(*b, stateOp);
}
// Assign top-level go guard in the transition state.
size_t topLevelGoIdx = nGroups;
assignStateTransitionGuard(*b, entryState->getState(),
{machineOp.getArgument(topLevelGoIdx)});
// Assign top-level done in the exit state.
assignStateOutputOperands(*b, exitState->getState(),
/*topLevelDone=*/true);
}
void MaterializeCalyxToFSMPass::assignAttributes() {
// sGroupDoneInputs is a mapping from group name to the index of the
// corresponding done input port.
llvm::SmallVector<NamedAttribute> groupDoneInputs;
for (size_t i = 0; i < referencedGroups.size(); ++i)
groupDoneInputs.push_back({referencedGroups[i], b->getI64IntegerAttr(i)});
machineOp->setAttr(calyxToFSM::sGroupDoneInputs,
b->getDictionaryAttr(groupDoneInputs));
// sGroupGoOutputs is a mapping from group name to the index of the
// corresponding go output port.
llvm::SmallVector<NamedAttribute> groupGoOutputs;
for (size_t i = 0; i < referencedGroups.size(); ++i)
groupGoOutputs.push_back({referencedGroups[i], b->getI64IntegerAttr(i)});
machineOp->setAttr(calyxToFSM::sGroupGoOutputs,
b->getDictionaryAttr(groupGoOutputs));
// Assign top level go/done attributes
machineOp->setAttr(calyxToFSM::sFSMTopLevelGoIndex,
b->getI64IntegerAttr(referencedGroups.size()));
machineOp->setAttr(calyxToFSM::sFSMTopLevelDoneIndex,
b->getI64IntegerAttr(referencedGroups.size()));
}
void MaterializeCalyxToFSMPass::runOnOperation() {
ComponentOp component = getOperation();
auto *ctx = &getContext();
auto b = OpBuilder(ctx);
auto builder = OpBuilder(ctx);
b = &builder;
auto controlOp = component.getControlOp();
machineOp =
dyn_cast_or_null<fsm::MachineOp>(controlOp.getBodyBlock()->front());
@ -131,8 +223,8 @@ void MaterializeCalyxToFSMPass::runOnOperation() {
// Ensure a well-formed FSM.
auto graph = FSMGraph(machineOp);
auto *entryState = graph.lookup(b.getStringAttr(calyxToFSM::sEntryStateName));
auto *exitState = graph.lookup(b.getStringAttr(calyxToFSM::sExitStateName));
entryState = graph.lookup(b->getStringAttr(calyxToFSM::sEntryStateName));
exitState = graph.lookup(b->getStringAttr(calyxToFSM::sExitStateName));
if (!(entryState && exitState)) {
machineOp.emitOpError()
@ -142,49 +234,9 @@ void MaterializeCalyxToFSMPass::runOnOperation() {
return;
}
// Walk the states of the machine and gather the relation between states and
// the groups which they enable as well as the set of all enabled states.
for (auto stateOp : machineOp.getOps<fsm::StateOp>()) {
for (auto enableOp : llvm::make_early_inc_range(
stateOp.getOutput().getOps<calyx::EnableOp>())) {
auto groupName = enableOp.getGroupNameAttr().getAttr();
stateEnables[stateOp].insert(groupName);
referencedGroups.insert(groupName);
// Erase the enable op now that we've recorded the information.
enableOp.erase();
}
}
// Materialize the top-level I/O ports of the fsm.machine. We add an in- and
// output for every unique group referenced within the machine, as well as an
// additional in- and output to represent the top-level "go" input and "done"
// output ports.
SmallVector<Type> ioTypes = SmallVector<Type>(
referencedGroups.size() + /*top-level go/done*/ 1, b.getI1Type());
size_t nGroups = ioTypes.size() - 1;
machineOp.setType(b.getFunctionType(ioTypes, ioTypes));
assert(machineOp.getBody().getNumArguments() == 0 &&
"expected no inputs to the FSM");
machineOp.getBody().addArguments(
ioTypes, SmallVector<Location, 4>(ioTypes.size(), b.getUnknownLoc()));
// Build output assignments and transition guards in every state. We here
// assume that the ordering of states in referencedGroups is fixed and
// deterministic, since it is used as an analogue for port I/O ordering.
for (auto stateOp : machineOp.getOps<fsm::StateOp>()) {
assignStateOutputOperands(b, stateOp,
/*topLevelDone=*/false);
assignStateTransitionGuard(b, stateOp);
}
// Assign top-level go guard in the transition state.
size_t topLevelGoIdx = nGroups;
assignStateTransitionGuard(b, entryState->getState(),
{machineOp.getArgument(topLevelGoIdx)});
// Assign top-level done in the exit state.
assignStateOutputOperands(b, exitState->getState(),
/*topLevelDone=*/true);
walkMachine();
materializeGroupIO();
assignAttributes();
}
std::unique_ptr<mlir::Pass> circt::createMaterializeCalyxToFSMPass() {

View File

@ -0,0 +1,308 @@
//===- RemoveGroupsFromFSM.cpp - Remove Groups Pass -------------*- C++ -*-===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// Contains the definitions of the Remove Groups pass.
//
//===----------------------------------------------------------------------===//
#include "../PassDetail.h"
#include "circt/Conversion/CalyxToFSM.h"
#include "circt/Dialect/Calyx/CalyxHelpers.h"
#include "circt/Dialect/Comb/CombOps.h"
#include "circt/Dialect/FSM/FSMGraph.h"
#include "circt/Support/BackedgeBuilder.h"
#include "circt/Support/LLVM.h"
#include "mlir/IR/BuiltinTypes.h"
#include "mlir/IR/OperationSupport.h"
#include "llvm/ADT/STLExtras.h"
using namespace circt;
using namespace calyx;
using namespace mlir;
using namespace fsm;
namespace {
struct CalyxRemoveGroupsFromFSM
: public CalyxRemoveGroupsFromFSMBase<CalyxRemoveGroupsFromFSM> {
void runOnOperation() override;
// Outlines the `fsm.machine` operation from within the `calyx.control`
// operation to the module scope, and instantiates the FSM. By doing so, we
// record the association between FSM outputs and group go signals as well as
// FSM inputs, which are backedges to the group done signals.
LogicalResult outlineMachine();
/// Makes several modifications to the operations of a GroupOp:
/// 1. Assign the 'done' signal of the component with the done_op of the top
/// level control group.
/// 2. Append the 'go' signal of the component to guard of each assignment.
/// 3. Replace all uses of GroupGoOp with the respective guard, and delete the
/// GroupGoOp.
/// 4. Remove the GroupDoneOp.
LogicalResult modifyGroupOperations();
/// Inlines each group in the WiresOp.
void inlineGroups();
/// A handle to the machine under transformation.
MachineOp machineOp;
// A handle to the component op under transformation.
ComponentOp componentOp;
OpBuilder *b;
BackedgeBuilder *bb;
// A mapping between group names and their 'go' inputs generated by the FSM.
DenseMap<StringAttr, Value> groupGoSignals;
// A mapping between group names and their 'done' output wires sent to
// the FSM.
DenseMap<StringAttr, calyx::WireLibOp> groupDoneWires;
};
} // end anonymous namespace
LogicalResult CalyxRemoveGroupsFromFSM::modifyGroupOperations() {
auto loc = componentOp.getLoc();
for (auto group : componentOp.getWiresOp().getOps<GroupOp>()) {
auto groupGo = group.getGoOp();
if (groupGo)
return emitError(loc)
<< "This pass does not need `calyx.group_go` operations.";
auto groupDone = group.getDoneOp();
if (!groupDone)
return emitError(loc) << "Group " << group.getSymName()
<< " does not have a `calyx.group_done` operation";
// Update group assignments to guard with the group go signal.
auto fsmGroupGo = groupGoSignals.find(group.getSymNameAttr());
assert(fsmGroupGo != groupGoSignals.end() &&
"Could not find FSM go signal for group");
updateGroupAssignmentGuards(*b, group, fsmGroupGo->second);
// Create a calyx wire for the group done signal, and assign it to the
// expression of the group_done operation.
auto doneWireIt = groupDoneWires.find(group.getSymNameAttr());
assert(doneWireIt != groupDoneWires.end() &&
"Could not find FSM done backedge for group");
auto doneWire = doneWireIt->second;
b->setInsertionPointToEnd(componentOp.getWiresOp().getBodyBlock());
b->create<calyx::AssignOp>(loc, doneWire.getIn(), groupDone.getSrc(),
groupDone.getGuard());
groupDone.erase();
}
return success();
}
/// Inlines each group in the WiresOp.
void CalyxRemoveGroupsFromFSM::inlineGroups() {
auto &wiresRegion = componentOp.getWiresOp().getRegion();
auto &wireBlocks = wiresRegion.getBlocks();
auto lastBlock = wiresRegion.end();
// Inline the body of each group as a Block into the WiresOp.
wiresRegion.walk([&](GroupOp group) {
wireBlocks.splice(lastBlock, group.getRegion().getBlocks());
group->erase();
});
// Merge the operations of each Block into the first block of the WiresOp.
auto firstBlock = wireBlocks.begin();
for (auto it = firstBlock, e = lastBlock; it != e; ++it) {
if (it == firstBlock)
continue;
firstBlock->getOperations().splice(firstBlock->end(), it->getOperations());
}
// Erase the (now) empty blocks.
while (&wiresRegion.front() != &wiresRegion.back())
wiresRegion.back().erase();
}
LogicalResult CalyxRemoveGroupsFromFSM::outlineMachine() {
// Walk all operations within the machine and gather the SSA values which are
// referenced in case they are not defined within the machine.
// MapVector ensures determinism.
llvm::MapVector<Value, SmallVector<Operation *>> referencedValues;
machineOp.walk([&](Operation *op) {
for (auto &operand : op->getOpOperands()) {
if (auto barg = operand.get().dyn_cast<BlockArgument>()) {
if (barg.getOwner()->getParentOp() == machineOp)
continue;
// A block argument defined outside of the machineOp.
referencedValues[operand.get()].push_back(op);
} else {
auto *defOp = operand.get().getDefiningOp();
auto machineOpParent = defOp->getParentOfType<MachineOp>();
if (machineOpParent && machineOpParent == machineOp)
continue;
referencedValues[operand.get()].push_back(op);
}
}
});
// Add a new input to the machine for each referenced SSA value and replace
// all uses of the value with the new input.
DenseMap<Value, size_t> ssaInputIndices;
auto machineOutputTypes = machineOp.getFunctionType().getResults();
auto currentInputs = machineOp.getFunctionType().getInputs();
llvm::SmallVector<Type> machineInputTypes(currentInputs);
for (auto &[value, users] : referencedValues) {
ssaInputIndices[value] = machineOp.getBody().getNumArguments();
auto t = value.getType();
auto arg = machineOp.getBody().addArgument(t, b->getUnknownLoc());
machineInputTypes.push_back(t);
for (auto *user : users) {
for (auto &operand : user->getOpOperands()) {
if (operand.get() == value)
operand.set(arg);
}
}
}
// Update the machineOp type.
machineOp.setType(b->getFunctionType(machineInputTypes, machineOutputTypes));
// Move the machine to module scope
machineOp->moveBefore(componentOp);
size_t nMachineInputs = machineOp.getBody().getNumArguments();
// Create an fsm.hwinstance in the Calyx component scope with backedges for
// the group done inputs.
auto groupDoneInputsAttr =
machineOp->getAttrOfType<DictionaryAttr>(calyxToFSM::sGroupDoneInputs);
auto groupGoOutputsAttr =
machineOp->getAttrOfType<DictionaryAttr>(calyxToFSM::sGroupGoOutputs);
if (!groupDoneInputsAttr || !groupGoOutputsAttr)
return emitError(machineOp.getLoc())
<< "MachineOp does not have a " << calyxToFSM::sGroupDoneInputs
<< " or " << calyxToFSM::sGroupGoOutputs
<< " attribute. Was --materialize-calyx-to-fsm run before "
"this pass?";
b->setInsertionPointToStart(&componentOp.getBody().front());
// Maintain a mapping between the FSM input index and the SSA value.
// We do this to sanity check that all inputs occur in the expected order.
DenseMap<size_t, Value> fsmInputMap;
// First we inspect the groupDoneInputsAttr map and create backedges.
for (auto &namedAttr : groupDoneInputsAttr.getValue()) {
auto name = namedAttr.getName();
auto idx = namedAttr.getValue().cast<IntegerAttr>();
auto inputIdx = idx.cast<IntegerAttr>().getInt();
if (fsmInputMap.count(inputIdx))
return emitError(machineOp.getLoc())
<< "MachineOp has duplicate input index " << idx;
// Create a wire for the group done input.
b->setInsertionPointToStart(&componentOp.getBody().front());
auto groupDoneWire = b->create<calyx::WireLibOp>(
componentOp.getLoc(), name.str() + "_done", b->getI1Type());
fsmInputMap[inputIdx] = groupDoneWire.getOut();
groupDoneWires[name] = groupDoneWire;
}
// Then we inspect the top level go/done attributes.
auto topLevelGoAttr =
machineOp->getAttrOfType<IntegerAttr>(calyxToFSM::sFSMTopLevelGoIndex);
if (!topLevelGoAttr)
return emitError(machineOp.getLoc())
<< "MachineOp does not have a " << calyxToFSM::sFSMTopLevelGoIndex
<< " attribute.";
fsmInputMap[topLevelGoAttr.getInt()] = componentOp.getGoPort();
auto topLevelDoneAttr =
machineOp->getAttrOfType<IntegerAttr>(calyxToFSM::sFSMTopLevelDoneIndex);
if (!topLevelDoneAttr)
return emitError(machineOp.getLoc())
<< "MachineOp does not have a " << calyxToFSM::sFSMTopLevelDoneIndex
<< " attribute.";
// Then we inspect the external SSA values.
for (auto [value, idx] : ssaInputIndices) {
if (fsmInputMap.count(idx))
return emitError(machineOp.getLoc())
<< "MachineOp has duplicate input index " << idx;
fsmInputMap[idx] = value;
}
if (fsmInputMap.size() != nMachineInputs)
return emitError(machineOp.getLoc())
<< "MachineOp has " << nMachineInputs
<< " inputs, but only recorded " << fsmInputMap.size()
<< " inputs. This either means that --materialize-calyx-to-fsm "
"failed or that there is a mismatch in the MachineOp attributes.";
// Convert the fsmInputMap to a list.
llvm::SmallVector<Value> fsmInputs;
for (size_t idx = 0; idx < nMachineInputs; ++idx) {
auto it = fsmInputMap.find(idx);
assert(it != fsmInputMap.end() && "Missing FSM input index");
fsmInputs.push_back(it->second);
}
// Instantiate the FSM.
auto fsmInstance = b->create<fsm::HWInstanceOp>(
machineOp.getLoc(), machineOutputTypes, b->getStringAttr("controller"),
machineOp.getSymNameAttr(), fsmInputs, componentOp.getClkPort(),
componentOp.getResetPort());
// Record the FSM output group go signals.
for (auto namedAttr : groupGoOutputsAttr.getValue()) {
auto name = namedAttr.getName();
auto idx = namedAttr.getValue().cast<IntegerAttr>().getInt();
groupGoSignals[name] = fsmInstance.getResult(idx);
}
// Assign FSM top level done to the component done.
b->setInsertionPointToEnd(componentOp.getWiresOp().getBodyBlock());
b->create<calyx::AssignOp>(machineOp.getLoc(), componentOp.getDonePort(),
fsmInstance.getResult(topLevelDoneAttr.getInt()));
return success();
}
void CalyxRemoveGroupsFromFSM::runOnOperation() {
componentOp = getOperation();
auto *ctx = componentOp.getContext();
auto builder = OpBuilder(ctx);
builder.setInsertionPointToStart(&componentOp.getBody().front());
auto backedgeBuilder = BackedgeBuilder(builder, componentOp.getLoc());
b = &builder;
bb = &backedgeBuilder;
// Locate the FSM machine in the control op..
auto machineOps = componentOp.getControlOp().getOps<fsm::MachineOp>();
if (std::distance(machineOps.begin(), machineOps.end()) != 1) {
emitError(componentOp.getLoc())
<< "Expected exactly one fsm.MachineOp in the control op";
signalPassFailure();
return;
}
machineOp = *machineOps.begin();
if (failed(outlineMachine()) || failed(modifyGroupOperations())) {
signalPassFailure();
return;
}
inlineGroups();
}
std::unique_ptr<mlir::Pass> circt::createRemoveGroupsFromFSMPass() {
return std::make_unique<CalyxRemoveGroupsFromFSM>();
}

View File

@ -614,7 +614,9 @@ MachineOpConverter::convertTransitions( // NOLINT(misc-no-recursion)
if (failed(guardOpRes))
return failure();
auto guard = cast<ReturnOp>(*guardOpRes).getOperand();
auto guardOp = cast<ReturnOp>(*guardOpRes);
assert(guardOp && "guard should be defined");
auto guard = guardOp.getOperand();
auto otherNextState =
convertTransitions(currentState, transitions.drop_front());
if (failed(otherNextState))

View File

@ -11,6 +11,7 @@
//===----------------------------------------------------------------------===//
#include "PassDetails.h"
#include "circt/Dialect/Calyx/CalyxHelpers.h"
#include "circt/Dialect/Calyx/CalyxOps.h"
#include "circt/Dialect/Calyx/CalyxPasses.h"
#include "circt/Support/LLVM.h"

View File

@ -26,22 +26,6 @@ namespace calyx {
#define GEN_PASS_CLASSES
#include "circt/Dialect/Calyx/CalyxPasses.h.inc"
/// Updates the guard of each assignment within a group with `op`.
template <typename Op>
static void updateGroupAssignmentGuards(OpBuilder &builder, GroupOp &group,
Op &op) {
group.walk([&](AssignOp assign) {
if (assign.getGuard())
// If the assignment is guarded already, take the bitwise & of the current
// guard and the group's go signal.
assign->setOperand(2, builder.create<comb::AndOp>(
group.getLoc(), assign.getGuard(), op, false));
else
// Otherwise, just insert it as the guard.
assign->insertOperands(2, {op});
});
}
} // namespace calyx
} // namespace circt

View File

@ -11,6 +11,7 @@
//===----------------------------------------------------------------------===//
#include "PassDetails.h"
#include "circt/Dialect/Calyx/CalyxHelpers.h"
#include "circt/Dialect/Calyx/CalyxOps.h"
#include "circt/Dialect/Calyx/CalyxPasses.h"
#include "circt/Support/LLVM.h"

View File

@ -111,13 +111,21 @@ void MachineOp::print(OpAsmPrinter &p) {
getArgAttrsAttrName(), getResAttrsAttrName());
}
static LogicalResult compareTypes(TypeRange rangeA, TypeRange rangeB) {
static LogicalResult compareTypes(Location loc, TypeRange rangeA,
TypeRange rangeB) {
if (rangeA.size() != rangeB.size())
return failure();
return emitError(loc) << "mismatch in number of types compared ("
<< rangeA.size() << " != " << rangeB.size() << ")";
for (auto zip : llvm::zip(rangeA, rangeB))
if (std::get<0>(zip) != std::get<1>(zip))
return failure();
size_t index = 0;
for (auto zip : llvm::zip(rangeA, rangeB)) {
auto typeA = std::get<0>(zip);
auto typeB = std::get<1>(zip);
if (typeA != typeB)
return emitError(loc) << "type mismatch at index " << index << " ("
<< typeA << " != " << typeB << ")";
++index;
}
return success();
}
@ -130,7 +138,8 @@ LogicalResult MachineOp::verify() {
// Verify that the argument list of the function and the arg list of the entry
// block line up. The trait already verified that the number of arguments is
// the same between the signature and the block.
if (failed(compareTypes(getArgumentTypes(), front().getArgumentTypes())))
if (failed(compareTypes(getLoc(), getArgumentTypes(),
front().getArgumentTypes())))
return emitOpError(
"entry block argument types must match the machine input types");
@ -196,7 +205,7 @@ static LogicalResult verifyCallerTypes(OpType op) {
return op.emitError("cannot find machine definition");
// Check operand types first.
if (failed(compareTypes(machine.getArgumentTypes(),
if (failed(compareTypes(op.getLoc(), machine.getArgumentTypes(),
op.getInputs().getTypes()))) {
auto diag =
op.emitOpError("operand types must match the machine input types");
@ -205,8 +214,8 @@ static LogicalResult verifyCallerTypes(OpType op) {
}
// Check result types.
if (failed(
compareTypes(machine.getResultTypes(), op.getOutputs().getTypes()))) {
if (failed(compareTypes(op.getLoc(), machine.getResultTypes(),
op.getOutputs().getTypes()))) {
auto diag =
op.emitOpError("result types must match the machine output types");
diag.attachNote(machine->getLoc()) << "original machine declared here";
@ -326,7 +335,8 @@ LogicalResult OutputOp::verify() {
// Verify that the result list of the machine and the operand list of the
// OutputOp line up.
auto machine = (*this)->getParentOfType<MachineOp>();
if (failed(compareTypes(machine.getResultTypes(), getOperandTypes())))
if (failed(
compareTypes(getLoc(), machine.getResultTypes(), getOperandTypes())))
return emitOpError("operand types must match the machine output types");
return success();

View File

@ -1,6 +1,6 @@
// RUN: circt-opt --split-input-file -pass-pipeline='builtin.module(calyx.component(lower-calyx-to-fsm))' %s | FileCheck %s
// CHECK: fsm.machine @control() attributes {compiledGroups = [@true, @false, @cond], initialState = "fsm_entry"} {
// CHECK: fsm.machine @control()
// CHECK-NEXT: fsm.state @fsm_entry output {
// CHECK-NEXT: fsm.output
// CHECK-NEXT: } transitions {

View File

@ -1,6 +1,6 @@
// RUN: circt-opt --split-input-file -pass-pipeline='builtin.module(calyx.component(lower-calyx-to-fsm))' %s | FileCheck %s
// CHECK: fsm.machine @control() attributes {compiledGroups = [@C, @B, @A], initialState = "fsm_entry"} {
// CHECK: fsm.machine @control()
// CHECK-NEXT: fsm.state @fsm_entry output {
// CHECK-NEXT: fsm.output
// CHECK-NEXT: } transitions {

View File

@ -1,6 +1,6 @@
// RUN: circt-opt --split-input-file -pass-pipeline='builtin.module(calyx.component(lower-calyx-to-fsm))' %s | FileCheck %s
// CHECK: fsm.machine @control() attributes {compiledGroups = [@do_add, @do_add, @do_add, @cond], initialState = "fsm_entry"} {
// CHECK: fsm.machine @control()
// CHECK-NEXT: fsm.state @fsm_entry output {
// CHECK-NEXT: fsm.output
// CHECK-NEXT: } transitions {

View File

@ -1,9 +1,9 @@
// RUN: circt-opt -pass-pipeline='builtin.module(calyx.component(materialize-calyx-to-fsm))' %s | FileCheck %s
// CHECK: fsm.machine @control(%[[A_DONE:.*]]: i1, %[[B_DONE:.*]]: i1, %[[COND_DONE:.*]]: i1, %[[TOP_LEVEL_GO:.*]]: i1) -> (i1, i1, i1, i1) attributes {compiledGroups = [@A, @B, @cond], initialState = "[[FSM_ENTRY:.*]]"} {
// CHECK: fsm.machine @control(%[[A_DONE:.*]]: i1, %[[B_DONE:.*]]: i1, %[[COND_DONE:.*]]: i1, %[[TOP_LEVEL_GO:.*]]: i1) -> (i1, i1, i1, i1)
// CHECK-NEXT: %[[C1:.*]] = hw.constant true
// CHECK-NEXT: %[[C0:.*]] = hw.constant false
// CHECK-NEXT: fsm.state @[[FSM_ENTRY]] output {
// CHECK-NEXT: fsm.state @fsm_entry output {
// CHECK-NEXT: fsm.output %[[C0]], %[[C0]], %[[C0]], %[[C0]] : i1, i1, i1, i1
// CHECK-NEXT: } transitions {
// CHECK-NEXT: fsm.transition @[[SEQ_0_COND:.*]] guard {

View File

@ -0,0 +1,145 @@
// RUN: circt-opt --split-input-file --calyx-remove-groups-fsm %s | FileCheck %s
// CHECK-LABEL: fsm.machine @control(
// CHECK-SAME: %[[VAL_0:.*]]: i1, %[[VAL_1:.*]]: i1, %[[VAL_2:.*]]: i1, %[[VAL_3:.*]]: i1, %[[VAL_4:.*]]: i8, %[[VAL_5:.*]]: i8) -> (i1, i1, i1, i1)
// CHECK: %[[VAL_6:.*]] = hw.constant true
// CHECK: %[[VAL_7:.*]] = hw.constant false
// CHECK: fsm.state @fsm_entry output {
// CHECK: fsm.output %[[VAL_7]], %[[VAL_7]], %[[VAL_7]], %[[VAL_7]] : i1, i1, i1, i1
// CHECK: } transitions {
// CHECK: fsm.transition @seq_0_A guard {
// CHECK: %[[VAL_8:.*]] = comb.icmp eq %[[VAL_4]], %[[VAL_5]] : i8
// CHECK: fsm.return %[[VAL_8]]
// CHECK: }
// CHECK: }
// CHECK: fsm.state @fsm_exit output {
// CHECK: fsm.output %[[VAL_7]], %[[VAL_7]], %[[VAL_7]], %[[VAL_6]] : i1, i1, i1, i1
// CHECK: }
// CHECK: fsm.state @seq_2_C output {
// CHECK: fsm.output %[[VAL_6]], %[[VAL_7]], %[[VAL_7]], %[[VAL_7]] : i1, i1, i1, i1
// CHECK: } transitions {
// CHECK: fsm.transition @fsm_exit guard {
// CHECK: fsm.return %[[VAL_0]]
// CHECK: }
// CHECK: }
// CHECK: fsm.state @seq_1_B output {
// CHECK: fsm.output %[[VAL_7]], %[[VAL_6]], %[[VAL_7]], %[[VAL_7]] : i1, i1, i1, i1
// CHECK: } transitions {
// CHECK: fsm.transition @seq_2_C guard {
// CHECK: fsm.return %[[VAL_1]]
// CHECK: }
// CHECK: }
// CHECK: fsm.state @seq_0_A output {
// CHECK: fsm.output %[[VAL_7]], %[[VAL_7]], %[[VAL_6]], %[[VAL_7]] : i1, i1, i1, i1
// CHECK: } transitions {
// CHECK: fsm.transition @seq_1_B guard {
// CHECK: fsm.return %[[VAL_2]]
// CHECK: }
// CHECK: }
// CHECK: }
// CHECK-LABEL: calyx.component @main(
// CHECK-SAME: %[[VAL_0:.*]]: i1 {go},
// CHECK-SAME: %[[VAL_1:.*]]: i1 {reset},
// CHECK-SAME: %[[VAL_2:.*]]: i1 {clk}) -> (
// CHECK-SAME: %[[VAL_3:.*]]: i1 {done}) {
// CHECK: %[[VAL_4:.*]], %[[VAL_5:.*]] = calyx.std_wire @C_done : i1, i1
// CHECK: %[[VAL_6:.*]]:4 = fsm.hw_instance "controller" @control(%[[VAL_5]], %[[VAL_7:.*]], %[[VAL_8:.*]], %[[VAL_0]], %[[VAL_9:.*]], %[[VAL_10:.*]]), clock %[[VAL_2]], reset %[[VAL_1]] : (i1, i1, i1, i1, i8, i8) -> (i1, i1, i1, i1)
// CHECK: %[[VAL_11:.*]], %[[VAL_7]] = calyx.std_wire @B_done : i1, i1
// CHECK: %[[VAL_12:.*]], %[[VAL_8]] = calyx.std_wire @A_done : i1, i1
// CHECK: %[[VAL_13:.*]] = hw.constant 2 : i8
// CHECK: %[[VAL_14:.*]] = hw.constant 1 : i8
// CHECK: %[[VAL_15:.*]] = hw.constant 0 : i8
// CHECK: %[[VAL_16:.*]] = hw.constant true
// CHECK: %[[VAL_10]], %[[VAL_17:.*]], %[[VAL_18:.*]], %[[VAL_19:.*]], %[[VAL_20:.*]], %[[VAL_21:.*]] = calyx.register @a : i8, i1, i1, i1, i8, i1
// CHECK: %[[VAL_9]], %[[VAL_22:.*]], %[[VAL_23:.*]], %[[VAL_24:.*]], %[[VAL_25:.*]], %[[VAL_26:.*]] = calyx.register @b : i8, i1, i1, i1, i8, i1
// CHECK: %[[VAL_27:.*]], %[[VAL_28:.*]], %[[VAL_29:.*]], %[[VAL_30:.*]], %[[VAL_31:.*]], %[[VAL_32:.*]] = calyx.register @c : i8, i1, i1, i1, i8, i1
// CHECK: calyx.wires {
// CHECK: %[[VAL_33:.*]] = calyx.undef : i1
// CHECK: calyx.assign %[[VAL_3]] = %[[VAL_6]]#3 : i1
// CHECK: calyx.assign %[[VAL_12]] = %[[VAL_21]] : i1
// CHECK: calyx.assign %[[VAL_11]] = %[[VAL_26]] : i1
// CHECK: calyx.assign %[[VAL_4]] = %[[VAL_32]] : i1
// CHECK: calyx.assign %[[VAL_10]] = %[[VAL_6]]#2 ? %[[VAL_15]] : i8
// CHECK: calyx.assign %[[VAL_17]] = %[[VAL_6]]#2 ? %[[VAL_16]] : i1
// CHECK: calyx.assign %[[VAL_9]] = %[[VAL_6]]#1 ? %[[VAL_14]] : i8
// CHECK: calyx.assign %[[VAL_22]] = %[[VAL_6]]#1 ? %[[VAL_16]] : i1
// CHECK: calyx.assign %[[VAL_27]] = %[[VAL_6]]#0 ? %[[VAL_13]] : i8
// CHECK: calyx.assign %[[VAL_28]] = %[[VAL_6]]#0 ? %[[VAL_16]] : i1
// CHECK: }
// CHECK: calyx.control {
// CHECK: }
// CHECK: }
calyx.component @main(%go: i1 {go}, %reset: i1 {reset}, %clk: i1 {clk}) -> (%done: i1 {done}) {
%c2_i8 = hw.constant 2 : i8
%c1_i8 = hw.constant 1 : i8
%c0_i8 = hw.constant 0 : i8
%true = hw.constant true
%a.in, %a.write_en, %a.clk, %a.reset, %a.out, %a.done = calyx.register @a : i8, i1, i1, i1, i8, i1
%b.in, %b.write_en, %b.clk, %b.reset, %b.out, %b.done = calyx.register @b : i8, i1, i1, i1, i8, i1
%c.in, %c.write_en, %c.clk, %c.reset, %c.out, %c.done = calyx.register @c : i8, i1, i1, i1, i8, i1
calyx.wires {
%0 = calyx.undef : i1
calyx.group @A {
calyx.assign %a.in = %c0_i8 : i8
calyx.assign %a.write_en = %true : i1
calyx.group_done %a.done : i1
}
calyx.group @B {
calyx.assign %b.in = %c1_i8 : i8
calyx.assign %b.write_en = %true : i1
calyx.group_done %b.done : i1
}
calyx.group @C {
calyx.assign %c.in = %c2_i8 : i8
calyx.assign %c.write_en = %true : i1
calyx.group_done %c.done : i1
}
}
calyx.control {
fsm.machine @control(%arg0: i1, %arg1: i1, %arg2: i1, %arg3: i1) -> (i1, i1, i1, i1) attributes
{compiledGroups = [@C, @B, @A],
calyx.fsm_group_done_inputs = {A = 2 : i64, B = 1 : i64, C = 0 : i64},
calyx.fsm_group_go_outputs = {A = 2 : i64, B = 1 : i64, C = 0 : i64},
calyx.fsm_top_level_done = 3 : i64,
calyx.fsm_top_level_go = 3 : i64,
initialState = "fsm_entry"} {
%true_0 = hw.constant true
%false = hw.constant false
fsm.state @fsm_entry output {
fsm.output %false, %false, %false, %false : i1, i1, i1, i1
} transitions {
fsm.transition @seq_0_A guard {
%0 = comb.icmp eq %b.in, %a.in : i8
fsm.return %0
}
}
fsm.state @fsm_exit output {
fsm.output %false, %false, %false, %true_0 : i1, i1, i1, i1
} transitions {
}
fsm.state @seq_2_C output {
fsm.output %true_0, %false, %false, %false : i1, i1, i1, i1
} transitions {
fsm.transition @fsm_exit guard {
fsm.return %arg0
}
}
fsm.state @seq_1_B output {
fsm.output %false, %true_0, %false, %false : i1, i1, i1, i1
} transitions {
fsm.transition @seq_2_C guard {
fsm.return %arg1
}
}
fsm.state @seq_0_A output {
fsm.output %false, %false, %true_0, %false : i1, i1, i1, i1
} transitions {
fsm.transition @seq_1_B guard {
fsm.return %arg2
}
}
}
}
}

View File

@ -12,6 +12,7 @@ fsm.machine @foo(%arg0: i1) -> i1 attributes {initialState = "IDLE"} {
fsm.state @IDLE output {
%true = arith.constant true
// expected-error @+2 {{mismatch in number of types compared (1 != 2)}}
// expected-error @+1 {{'fsm.output' op operand types must match the machine output types}}
fsm.output %true, %true : i1, i1
} transitions {