[SSP] Allow standalone use of `OperatorLibraryOp`. (#4222)

This commit is contained in:
Julian Oppermann 2022-11-11 09:54:09 +01:00 committed by GitHub
parent 6ed5055917
commit 3e65bb13d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 250 additions and 58 deletions

View File

@ -126,16 +126,25 @@ problem instances?
### Use of container-like operations instead of regions in `InstanceOp`
This dialect defines the `OperatorLibraryOp` and `DependenceGraphOp` to
serve exclusively as the first and second operation in an `InstanceOp`'s region.
The alternative of using two regions on the `InstanceOp` is not applicable,
because the `InstanceOp` then needs to provide a symbol table, but the upstream
This dialect defines the `OperatorLibraryOp` and `DependenceGraphOp` to serve as
the first and second operation in an `InstanceOp`'s region. The alternative of
using two regions on the `InstanceOp` is not applicable, because the
`InstanceOp` then needs to provide a symbol table, but the upstream
`SymbolTable` trait enforces single-region ops. Lastly, we also considered using
a single graph region to hold both `OperatorTypeOp`s and `OperationOp`s, but
discarded that design because it cannot be safely roundtripped via a
`circt::scheduling::Problem` (internally, registered operator types and
operations are separate lists).
### Stand-alone use of the `OperatorLibraryOp`
The `OperatorLibraryOp` can be named and used outside of an `InstanceOp`. This
is useful to share operator type definitions across multiple instances. In
addition, until CIRCT gains better infrastructure to manage predefined hardware
modules and their properties, such a stand-alone `OperatorLibraryOp` can also
act as an interim solution to represent operator libraries for scheduling
clients.
### Use of SSA operands _and_ symbol references to encode dependences
This is required to faithfully reproduce the internal modeling in the scheduling

View File

@ -45,7 +45,7 @@ include "PropertyBase.td"
// Problem
def LinkedOperatorTypeProp : OperationProperty<SSPDialect,
"LinkedOperatorType", "::mlir::FlatSymbolRefAttr", "::circt::scheduling::Problem"> {
"LinkedOperatorType", "::mlir::SymbolRefAttr", "::circt::scheduling::Problem"> {
let mnemonic = "opr";
let unwrapValue = [{ getValue().getLeafReference() }];
let wrapValue = [{ ::mlir::FlatSymbolRefAttr::get(ctx, value) }];

View File

@ -19,7 +19,7 @@ class SSPOp<string mnemonic, list<Trait> traits = []> :
def InstanceOp : SSPOp<"instance",
[NoRegionArguments, SingleBlock, NoTerminator,
IsolatedFromAbove, OpAsmOpInterface]> {
IsolatedFromAbove, OpAsmOpInterface, SymbolTable, Symbol]> {
let summary = "Instance of a static scheduling problem.";
let description = [{
This operation represents an instance of a static scheduling problem,
@ -33,7 +33,7 @@ def InstanceOp : SSPOp<"instance",
**Example**
```mlir
ssp.instance "canis14_fig2" of "ModuloProblem" [II<3>] {
ssp.instance @canis14_fig2 of "ModuloProblem" [II<3>] {
library {
operator_type @MemPort [latency<1>, limit<1>]
operator_type @Add [latency<1>]
@ -48,11 +48,11 @@ def InstanceOp : SSPOp<"instance",
```
}];
let arguments = (ins StrAttr:$instanceName, StrAttr:$problemName,
let arguments = (ins SymbolNameAttr:$sym_name, StrAttr:$problemName,
OptionalAttr<ArrayAttr>:$properties);
let regions = (region SizedRegion<1>:$body);
let assemblyFormat = [{
$instanceName `of` $problemName custom<Properties>($properties) $body attr-dict
$sym_name `of` $problemName custom<Properties>($properties) $body attr-dict
}];
let hasVerifier = true;
@ -66,6 +66,9 @@ def InstanceOp : SSPOp<"instance",
return &getBody().getBlocks().front();
}
// The symbol is actually the "instance name"
::mlir::StringAttr getInstanceNameAttr() { return getSymNameAttr(); }
// Access to container ops
::circt::ssp::OperatorLibraryOp getOperatorLibrary();
::circt::ssp::DependenceGraphOp getDependenceGraph();
@ -76,7 +79,7 @@ def InstanceOp : SSPOp<"instance",
OpBuilder<(ins "::mlir::StringAttr":$instanceName,
"::mlir::StringAttr":$problemName,
CArg<"::mlir::ArrayAttr", "::mlir::ArrayAttr()">:$properties), [{
$_state.addAttribute($_builder.getStringAttr("instanceName"), instanceName);
$_state.addAttribute(::mlir::SymbolTable::getSymbolAttrName(), instanceName);
$_state.addAttribute($_builder.getStringAttr("problemName"), problemName);
if (properties)
$_state.addAttribute($_builder.getStringAttr("properties"), properties);
@ -86,11 +89,55 @@ def InstanceOp : SSPOp<"instance",
];
}
class ContainerOp<string mnemonic, list<Trait> traits = []>
: SSPOp<mnemonic, traits # [NoRegionArguments, SingleBlock, NoTerminator,
SymbolTable, OpAsmOpInterface, HasParent<"InstanceOp">]> {
def OperatorLibraryOp : SSPOp<"library",
[NoRegionArguments, SingleBlock,
NoTerminator, OpAsmOpInterface, SymbolTable, Symbol]> {
let summary = "Container for operator types.";
let description = [{
The operator library abstracts the characteristics of the target
architecture/IR (onto which the source graph is scheduled), represented by
the individual `OperatorTypeOp`s. This operation may be used outside of an
`InstanceOp`.
}];
let arguments = (ins OptionalAttr<SymbolNameAttr>:$sym_name);
let assemblyFormat = "($sym_name^)? $body attr-dict";
let regions = (region SizedRegion<1>:$body);
let extraClassDeclaration = [{
// OpAsmOpInterface
static ::llvm::StringRef getDefaultDialect() { return "ssp"; }
// SymbolUserOpInterface
static bool isOptionalSymbol() { return true; }
// Convenience
::mlir::Block *getBodyBlock() {
return &getBody().getBlocks().front();
}
}];
let skipDefaultBuilders = true;
let builders = [
OpBuilder<(ins ), [{
::mlir::Region* region = $_state.addRegion();
region->push_back(new ::mlir::Block());
}]>
];
}
def DependenceGraphOp : SSPOp<"graph",
[HasOnlyGraphRegion, NoRegionArguments,
SingleBlock, NoTerminator, OpAsmOpInterface, SymbolTable,
HasParent<"InstanceOp">]> {
let summary = "Container for (scheduling) operations.";
let description = [{
The dependence graph is spanned by `OperationOp`s (vertices) and a
combination of MLIR value uses and symbol references (edges).
}];
let assemblyFormat = "$body attr-dict";
let regions = (region SizedRegion<1>:$body);
let extraClassDeclaration = [{
// OpAsmOpInterface
@ -111,24 +158,6 @@ class ContainerOp<string mnemonic, list<Trait> traits = []>
];
}
def OperatorLibraryOp : ContainerOp<"library"> {
let summary = "Container for operator types.";
let description = [{
The operator library abstracts the characteristics of the target
architecture/IR (onto which the source graph is scheduled), represented by
the individual `OperatorTypeOp`s.
}];
}
def DependenceGraphOp : ContainerOp<"graph",
[RegionKindInterface, HasOnlyGraphRegion]> {
let summary = "Container for (scheduling) operations.";
let description = [{
The dependence graph is spanned by `OperationOp`s (vertices) and a
combination of MLIR value uses and symbol references (edges).
}];
}
def OperatorTypeOp : SSPOp<"operator_type",
[Symbol, HasParent<"OperatorLibraryOp">]> {
let summary = "Element of the target architecture/IR.";
@ -165,7 +194,9 @@ def OperationOp : SSPOp<"operation",
The `linkedOperatorType` property in the root `Problem` class is central to
the problem models, because it links operations to their properties in the
target IR. Therefore, the referenced operator type symbol is parsed/printed
right after the operation keyword in the custom assembly syntax.
right after the operation keyword in the custom assembly syntax. Flat symbol
references are resolved by name in the surrounding instance's operator
library. Nested references can point to arbitrary operator libraries.
**Examples**
```mlir
@ -180,6 +211,9 @@ def OperationOp : SSPOp<"operation",
// dependence properties
operation<@Barrier>(%2 [dist<1>], %5#1, @store_A [dist<3>])
// operator type in stand-alone library
%7 = operation<@MathLib::@Sqrt>(%6)
```
}];

View File

@ -20,6 +20,7 @@
#include "circt/Support/ValueMapper.h"
#include "mlir/IR/ImplicitLocOpBuilder.h"
#include "mlir/IR/SymbolTable.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/TypeSwitch.h"
@ -92,12 +93,37 @@ void loadInstanceProperties(ProblemT &prob, ArrayAttr props) {
}
}
/// Load the operator type represented by \p oprOp into \p prob under a unique
/// name informed by \p oprIds, and attempt to set its properties from the
/// given attribute classes. The registered name is returned. The template
/// instantiation fails if properties are incompatible with \p ProblemT.
template <typename ProblemT, typename... OperatorTypePropertyTs>
OperatorType loadOperatorType(ProblemT &prob, OperatorTypeOp oprOp,
SmallDenseMap<StringAttr, unsigned> &oprIds) {
OperatorType opr = oprOp.getNameAttr();
unsigned &id = oprIds[opr];
if (id > 0)
opr = StringAttr::get(opr.getContext(),
opr.getValue() + Twine('_') + Twine(id));
++id;
assert(!prob.hasOperatorType(opr));
prob.insertOperatorType(opr);
loadOperatorTypeProperties<ProblemT, OperatorTypePropertyTs...>(
prob, opr, oprOp.getPropertiesAttr());
return opr;
}
/// Construct an instance of \p ProblemT from \p instOp, and attempt to set
/// properties from the given attribute classes. The attribute tuples are used
/// solely for grouping/inferring the template parameter packs. The tuple
/// elements may therefore be unitialized objects. The template instantiation
/// fails if properties are incompatible with \p ProblemT.
///
/// Operations may link to operator types in other libraries, but the origin of
/// an operator type will not be preserved in the problem instance. As this
/// could lead to conflicts, operator types will be automatically renamed in the
/// returned instance.
///
/// Example: To load an instance of the `circt::scheduling::CyclicProblem` with
/// all its input and solution properties, call this as follows:
///
@ -121,11 +147,19 @@ ProblemT loadProblem(InstanceOp instOp,
loadInstanceProperties<ProblemT, InstancePropertyTs...>(
prob, instOp.getPropertiesAttr());
instOp.getOperatorLibrary().walk([&](OperatorTypeOp oprOp) {
OperatorType opr = oprOp.getNameAttr();
prob.insertOperatorType(opr);
loadOperatorTypeProperties<ProblemT, OperatorTypePropertyTs...>(
prob, opr, oprOp.getPropertiesAttr());
// Use IDs to disambiguate operator types with the same name defined in
// different libraries.
SmallDenseMap<OperatorType, unsigned> operatorTypeIds;
// Map `OperatorTypeOp`s to their (possibly uniqued) name in the problem
// instance.
SmallDenseMap<Operation *, OperatorType> operatorTypes;
// Register all operator types in the instance's library.
auto libraryOp = instOp.getOperatorLibrary();
libraryOp.walk([&](OperatorTypeOp oprOp) {
operatorTypes[oprOp] =
loadOperatorType<ProblemT, OperatorTypePropertyTs...>(prob, oprOp,
operatorTypeIds);
});
// Register all operations first, in order to retain their original order.
@ -134,6 +168,38 @@ ProblemT loadProblem(InstanceOp instOp,
prob.insertOperation(opOp);
loadOperationProperties<ProblemT, OperationPropertyTs...>(
prob, opOp, opOp.getPropertiesAttr());
// Nothing else to check if no linked operator type is set for `opOp`,
// because the operation doesn't carry a `LinkedOperatorTypeAttr`, or that
// class is not part of the `OperationPropertyTs` to load.
if (!prob.getLinkedOperatorType(opOp).has_value())
return;
// Otherwise, inspect the corresponding attribute to make sure the operator
// type is available.
SymbolRefAttr oprRef = opOp.getLinkedOperatorTypeAttr().getValue();
Operation *oprOp;
// 1) Look in the instance's library.
oprOp = SymbolTable::lookupSymbolIn(libraryOp, oprRef);
// 2) Try to resolve a nested reference to the instance's library.
if (!oprOp)
oprOp = SymbolTable::lookupSymbolIn(instOp, oprRef);
// 3) Look outside of the instance.
if (!oprOp)
oprOp =
SymbolTable::lookupNearestSymbolFrom(instOp->getParentOp(), oprRef);
assert(oprOp && isa<OperatorTypeOp>(oprOp)); // checked by verifier
// Load the operator type from `oprOp` if needed.
auto &opr = operatorTypes[oprOp];
if (!opr)
opr = loadOperatorType<ProblemT, OperatorTypePropertyTs...>(
prob, cast<OperatorTypeOp>(oprOp), operatorTypeIds);
// Update `opOp`'s property (may be a no-op if `opr` wasn't renamed).
prob.setLinkedOperatorType(opOp, opr);
});
// Then walk them again, and load auxiliary dependences as well as any

View File

@ -61,7 +61,7 @@ ParseResult OperationOp::parse(OpAsmParser &parser, OperationState &result) {
if (parser.parseLess())
return failure();
FlatSymbolRefAttr oprRef;
SymbolRefAttr oprRef;
auto parseSymbolResult = parser.parseOptionalAttribute(oprRef);
if (parseSymbolResult.has_value()) {
assert(succeeded(*parseSymbolResult));
@ -276,8 +276,18 @@ OperationOp::verifySymbolUses(SymbolTableCollection &symbolTable) {
// If a linkedOperatorType property is present, verify that it references a
// valid operator type.
if (auto linkedOpr = getLinkedOperatorTypeAttr()) {
FlatSymbolRefAttr oprRef = linkedOpr.getValue();
Operation *oprOp = symbolTable.lookupSymbolIn(libraryOp, oprRef);
SymbolRefAttr oprRef = linkedOpr.getValue();
Operation *oprOp;
// 1) Look in the instance's library.
oprOp = symbolTable.lookupSymbolIn(libraryOp, oprRef);
// 2) Try to resolve a nested reference to the instance's library.
if (!oprOp)
oprOp = symbolTable.lookupSymbolIn(instanceOp, oprRef);
// 3) Look outside of the instance.
if (!oprOp)
oprOp = symbolTable.lookupNearestSymbolFrom(instanceOp->getParentOp(),
oprRef);
if (!oprOp || !isa<OperatorTypeOp>(oprOp))
return emitError("Linked operator type property references invalid "
"operator type: ")

View File

@ -1,19 +1,19 @@
// RUN: circt-opt %s -split-input-file -verify-diagnostics
// expected-error @+1 {{must contain exactly one 'library' op and one 'graph' op}}
ssp.instance "containers_empty" of "Problem" {}
ssp.instance @containers_empty of "Problem" {}
// -----
// expected-error @+1 {{must contain the 'library' op followed by the 'graph' op}}
ssp.instance "containers_wrong_order" of "Problem" {
ssp.instance @containers_wrong_order of "Problem" {
graph {}
library {}
}
// -----
ssp.instance "deps_out_of_bounds" of "Problem" {
ssp.instance @deps_out_of_bounds of "Problem" {
library {}
graph {
// expected-error @+1 {{Operand index is out of bounds for def-use dependence attribute}}
@ -23,7 +23,7 @@ ssp.instance "deps_out_of_bounds" of "Problem" {
// -----
ssp.instance "deps_defuse_not_increasing" of "Problem" {
ssp.instance @deps_defuse_not_increasing of "Problem" {
library {}
graph {
%0:2 = ssp.operation<>()
@ -34,7 +34,7 @@ ssp.instance "deps_defuse_not_increasing" of "Problem" {
// -----
ssp.instance "deps_interleaved" of "Problem" {
ssp.instance @deps_interleaved of "Problem" {
library {}
graph {
%0 = operation<> @Op()
@ -45,7 +45,7 @@ ssp.instance "deps_interleaved" of "Problem" {
// -----
ssp.instance "deps_aux_not_consecutive" of "Problem" {
ssp.instance @deps_aux_not_consecutive of "Problem" {
library {}
graph {
operation<> @Op()
@ -56,7 +56,7 @@ ssp.instance "deps_aux_not_consecutive" of "Problem" {
// -----
ssp.instance "deps_aux_invalid" of "Problem" {
ssp.instance @deps_aux_invalid of "Problem" {
library {}
graph {
// expected-error @+1 {{Auxiliary dependence references invalid source operation: @InvalidOp}}
@ -66,10 +66,21 @@ ssp.instance "deps_aux_invalid" of "Problem" {
// -----
ssp.instance "linked_opr_invalid" of "Problem" {
ssp.instance @linked_opr_invalid of "Problem" {
library {}
graph {
// expected-error @+1 {{Linked operator type property references invalid operator type: @InvalidOpr}}
operation<@InvalidOpr>()
}
}
// -----
ssp.library @standalone {}
ssp.instance @standalone_opr_invalid of "Problem" {
library {}
graph {
// expected-error @+1 {{Linked operator type property references invalid operator type: @standalone::@InvalidOpr}}
operation<@standalone::@InvalidOpr>()
}
}

View File

@ -4,7 +4,7 @@
// 1) tests the plain parser/printer roundtrip.
// 2) roundtrips via the scheduling infra (i.e. populates a `Problem` instance and reconstructs the SSP IR from it.)
// CHECK: ssp.instance "no properties" of "Problem" {
// CHECK: ssp.instance @"no properties" of "Problem" {
// CHECK: library {
// CHECK: operator_type @NoProps
// CHECK: }
@ -15,7 +15,7 @@
// CHECK: operation<>(%[[op_0]], @Op0)
// CHECK: }
// CHECK: }
ssp.instance "no properties" of "Problem" {
ssp.instance @"no properties" of "Problem" {
library {
operator_type @NoProps
}
@ -27,7 +27,7 @@ ssp.instance "no properties" of "Problem" {
}
}
// CHECK: ssp.instance "arbitrary_latencies" of "Problem" {
// CHECK: ssp.instance @arbitrary_latencies of "Problem" {
// CHECK: library {
// CHECK: operator_type @unit [latency<1>]
// CHECK: operator_type @extr [latency<0>]
@ -45,7 +45,7 @@ ssp.instance "no properties" of "Problem" {
// CHECK: operation<@unit>(%[[op_5]]) [t<60>]
// CHECK: }
// CHECK: }
ssp.instance "arbitrary_latencies" of "Problem" {
ssp.instance @arbitrary_latencies of "Problem" {
library {
operator_type @unit [latency<1>]
operator_type @extr [latency<0>]
@ -64,7 +64,7 @@ ssp.instance "arbitrary_latencies" of "Problem" {
}
}
// CHECK: ssp.instance "self_arc" of "CyclicProblem" [II<3>] {
// CHECK: ssp.instance @self_arc of "CyclicProblem" [II<3>] {
// CHECK: library {
// CHECK: operator_type @unit [latency<1>]
// CHECK: operator_type @_3 [latency<3>]
@ -75,7 +75,7 @@ ssp.instance "arbitrary_latencies" of "Problem" {
// CHECK: operation<@unit>(%[[op_1]]) [t<4>]
// CHECK: }
// CHECK: }
ssp.instance "self_arc" of "CyclicProblem" [II<3>] {
ssp.instance @self_arc of "CyclicProblem" [II<3>] {
library {
operator_type @unit [latency<1>]
operator_type @_3 [latency<3>]
@ -87,7 +87,7 @@ ssp.instance "self_arc" of "CyclicProblem" [II<3>] {
}
}
// CHECK: ssp.instance "multiple_oprs" of "SharedOperatorsProblem" {
// CHECK: ssp.instance @multiple_oprs of "SharedOperatorsProblem" {
// CHECK: library {
// CHECK: operator_type @slowAdd [latency<3>, limit<2>]
// CHECK: operator_type @fastAdd [latency<1>, limit<1>]
@ -104,7 +104,7 @@ ssp.instance "self_arc" of "CyclicProblem" [II<3>] {
// CHECK: operation<@_1>() [t<10>]
// CHECK: }
// CHECK: }
ssp.instance "multiple_oprs" of "SharedOperatorsProblem" {
ssp.instance @multiple_oprs of "SharedOperatorsProblem" {
library {
operator_type @slowAdd [latency<3>, limit<2>]
operator_type @fastAdd [latency<1>, limit<1>]
@ -122,7 +122,7 @@ ssp.instance "multiple_oprs" of "SharedOperatorsProblem" {
}
}
// CHECK: ssp.instance "canis14_fig2" of "ModuloProblem" [II<3>] {
// CHECK: ssp.instance @canis14_fig2 of "ModuloProblem" [II<3>] {
// CHECK: library {
// CHECK: operator_type @MemPort [latency<1>, limit<1>]
// CHECK: operator_type @Add [latency<1>]
@ -136,7 +136,7 @@ ssp.instance "multiple_oprs" of "SharedOperatorsProblem" {
// CHECK: operation<@Implicit> @last(@store_A) [t<5>]
// CHECK: }
// CHECK: }
ssp.instance "canis14_fig2" of "ModuloProblem" [II<3>] {
ssp.instance @canis14_fig2 of "ModuloProblem" [II<3>] {
library {
operator_type @MemPort [latency<1>, limit<1>]
operator_type @Add [latency<1>]

View File

@ -0,0 +1,62 @@
// RUN: circt-opt %s | circt-opt | FileCheck %s
// RUN: circt-opt %s -test-ssp-roundtrip | circt-opt | FileCheck %s --check-prefix=INFRA
// 1) tests the plain parser/printer roundtrip.
// CHECK: ssp.library @Lib {
// CHECK: operator_type @Opr [latency<1>, limit<1>]
// CHECK: }
// CHECK: module @SomeModule {
// CHECK: ssp.library @Lib {
// CHECK: operator_type @Opr [latency<2>, limit<2>]
// CHECK: }
// CHECK: }
// CHECK: ssp.instance @SomeInstance of "ModuloProblem" {
// CHECK: library @InternalLib {
// CHECK: operator_type @Opr [latency<3>, limit<3>]
// CHECK: }
// CHECK: graph {
// CHECK: operation<@Opr>()
// CHECK: operation<@InternalLib::@Opr>()
// CHECK: operation<@SomeInstance::@InternalLib::@Opr>()
// CHECK: operation<@Lib::@Opr>()
// CHECK: operation<@SomeModule::@Lib::@Opr>()
// CHECK: }
// CHECK: }
// 2) Import/export via the scheduling infra (i.e. populates a `Problem` instance and reconstructs the SSP IR from it.)
// Operator types from stand-alone libraries are appended to the instance's internal library, whose name is not preserved.
// INFRA: ssp.instance @SomeInstance of "ModuloProblem" {
// INFRA: library {
// INFRA: operator_type @Opr [latency<3>, limit<3>]
// INFRA: operator_type @Opr_1 [latency<1>, limit<1>]
// INFRA: operator_type @Opr_2 [latency<2>, limit<2>]
// INFRA: }
// INFRA: graph {
// INFRA: operation<@Opr>()
// INFRA: operation<@Opr>()
// INFRA: operation<@Opr>()
// INFRA: operation<@Opr_1>()
// INFRA: operation<@Opr_2>()
// INFRA: }
// INFRA: }
ssp.library @Lib {
operator_type @Opr [latency<1>, limit<1>]
}
module @SomeModule {
ssp.library @Lib {
operator_type @Opr [latency<2>, limit<2>]
}
}
ssp.instance @SomeInstance of "ModuloProblem" {
library @InternalLib {
operator_type @Opr [latency<3>, limit<3>]
}
graph {
operation<@Opr>()
operation<@InternalLib::@Opr>()
operation<@SomeInstance::@InternalLib::@Opr>()
operation<@Lib::@Opr>()
operation<@SomeModule::@Lib::@Opr>()
}
}