From 1b177e95689e8e5a9102a379c532d6d625f75a4a Mon Sep 17 00:00:00 2001 From: Ben Vanik Date: Wed, 25 Oct 2023 11:10:08 -0700 Subject: [PATCH] Making flow.dispatch/stream.async.dispatch take multiple symbols. (#15295) stream.cmd.dispatch already supported this for making external hal.executable.variant ops work and by making this consistent up the stack it allows for the use of hal.executable.variant all the way up in flow. This will allow hal.dispatch.extern to expand to HAL ops instead of flow.executable and avoid the need for plumbing all of the HAL-specific behavior through those layers. --- .../Dialect/Flow/IR/FlowOpFolders.cpp | 28 +++++ .../iree/compiler/Dialect/Flow/IR/FlowOps.cpp | 107 +++++++++++++---- .../iree/compiler/Dialect/Flow/IR/FlowOps.td | 17 ++- .../Dialect/Flow/IR/test/dispatch_ops.mlir | 33 +++--- .../Flow/Transforms/AnnotateDispatches.cpp | 13 +- .../Transforms/DeduplicateExecutables.cpp | 17 ++- .../Flow/Transforms/DumpDispatchGraph.cpp | 4 +- .../Flow/Transforms/InjectDispatchTracing.cpp | 7 +- .../Transforms/InsertDispatchDebugTargets.cpp | 15 +-- .../test/deduplicate_executables.mlir | 67 ++++++----- .../Conversion/FlowToStream/Patterns.cpp | 2 +- .../Dialect/Stream/IR/StreamOpFolders.cpp | 42 ++++++- .../compiler/Dialect/Stream/IR/StreamOps.cpp | 112 ++++++++++-------- .../compiler/Dialect/Stream/IR/StreamOps.td | 11 +- .../Stream/Transforms/MaterializeBuiltins.cpp | 8 +- .../Stream/Transforms/ScheduleAllocation.cpp | 7 +- .../iree/compiler/Dialect/Util/IR/UtilOps.cpp | 7 ++ .../iree/compiler/Dialect/Util/IR/UtilOps.h | 3 + 18 files changed, 335 insertions(+), 165 deletions(-) diff --git a/compiler/src/iree/compiler/Dialect/Flow/IR/FlowOpFolders.cpp b/compiler/src/iree/compiler/Dialect/Flow/IR/FlowOpFolders.cpp index 0f13fc23c493..877b318518dc 100644 --- a/compiler/src/iree/compiler/Dialect/Flow/IR/FlowOpFolders.cpp +++ b/compiler/src/iree/compiler/Dialect/Flow/IR/FlowOpFolders.cpp @@ -702,6 +702,34 @@ void DispatchTensorStoreOp::getCanonicalizationPatterns( context); } +//===----------------------------------------------------------------------===// +// flow.dispatch +//===----------------------------------------------------------------------===// + +namespace { + +struct DeduplicateDispatchEntryRefs final + : public OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + LogicalResult matchAndRewrite(DispatchOp dispatchOp, + PatternRewriter &rewriter) const override { + auto originalAttr = dispatchOp.getEntryPointsAttr(); + auto newAttr = deduplicateArrayElements(originalAttr); + if (newAttr == originalAttr) + return failure(); + rewriter.updateRootInPlace( + dispatchOp, [&]() { dispatchOp.setEntryPointsAttr(newAttr); }); + return success(); + } +}; + +} // namespace + +void DispatchOp::getCanonicalizationPatterns(RewritePatternSet &results, + MLIRContext *context) { + results.insert(context); +} + //===----------------------------------------------------------------------===// // Tensor ops //===----------------------------------------------------------------------===// diff --git a/compiler/src/iree/compiler/Dialect/Flow/IR/FlowOps.cpp b/compiler/src/iree/compiler/Dialect/Flow/IR/FlowOps.cpp index 26dd4e9385a2..59a02c4b1038 100644 --- a/compiler/src/iree/compiler/Dialect/Flow/IR/FlowOps.cpp +++ b/compiler/src/iree/compiler/Dialect/Flow/IR/FlowOps.cpp @@ -332,6 +332,44 @@ static void printDispatchWorkgroupsCountRegion(OpAsmPrinter &p, Operation *op, printWorkgroupCountRegionWithoutKeyword(p, op, body); } +//===----------------------------------------------------------------------===// +// custom($entry_points) +//===----------------------------------------------------------------------===// + +static ParseResult parseDispatchEntryPoints(OpAsmParser &parser, + ArrayAttr &entryPointAttrsArray) { + SmallVector entryPointAttrs; + if (succeeded(parser.parseOptionalLBrace())) { + do { + SymbolRefAttr entryPointAttr; + if (failed(parser.parseAttribute(entryPointAttr))) + return failure(); + entryPointAttrs.push_back(entryPointAttr); + } while (succeeded(parser.parseOptionalComma())); + if (failed(parser.parseRBrace())) + return failure(); + } else { + SymbolRefAttr entryPointAttr; + if (failed(parser.parseAttribute(entryPointAttr))) + return failure(); + entryPointAttrs.push_back(entryPointAttr); + } + entryPointAttrsArray = parser.getBuilder().getArrayAttr(entryPointAttrs); + return success(); +} + +static void printDispatchEntryPoints(OpAsmPrinter &p, Operation *op, + ArrayAttr entryPointAttrs) { + if (entryPointAttrs.size() == 1) { + p.printAttribute(entryPointAttrs.getValue().front()); + } else { + p << '{'; + llvm::interleaveComma(entryPointAttrs, p.getStream(), + [&](Attribute attr) { p.printAttribute(attr); }); + p << '}'; + } +} + //===----------------------------------------------------------------------===// // flow.dispatch.region //===----------------------------------------------------------------------===// @@ -1329,7 +1367,7 @@ void DispatchOp::build(OpBuilder &builder, OperationState &state, ValueRange operands, ValueRange operandDims, ArrayAttr tiedOperands, ArrayRef attributes) { - state.addAttribute("entry_point", entryPoint); + state.addAttribute("entry_points", builder.getArrayAttr(entryPoint)); state.addOperands(workload); state.addTypes(resultTypes); state.addOperands(operands); @@ -1349,51 +1387,72 @@ void DispatchOp::build(OpBuilder &builder, OperationState &state, })); } -StringAttr DispatchOp::executable() { - return getEntryPoint().getRootReference(); -} - FunctionType DispatchOp::getEntryPointType() { SmallVector argTypes(operand_type_range{getArguments()}); return FunctionType::get(getContext(), argTypes, getResultTypes()); } +std::string DispatchOp::getEntryPointName() { + // Pick the first entry point we have. The common case is we only have one + // but frontends may provide multiple variants - they're all likely the + // same name but with slight differences and enough for a user to know what's + // happening. + auto anyEntryPoint = *getEntryPointRefs().begin(); + std::string entryPointName = + anyEntryPoint.getRootReference().getValue().str(); + for (FlatSymbolRefAttr nestedRef : anyEntryPoint.getNestedReferences()) { + entryPointName = (entryPointName + "::" + nestedRef.getValue()).str(); + } + return entryPointName; +} + std::pair DispatchOp::getTiedOperandsIndexAndLength() { return getODSOperandIndexAndLength(1); // $operands } LogicalResult DispatchOp::verify() { Operation *op = getOperation(); + + if (getEntryPoints().empty()) { + return op->emitOpError("at least one entry point reference is required"); + } + if (failed(verifyOpDynamicDims(op, getArguments(), getArgumentDims())) || failed(verifyOpDynamicDims(op, getResults(), getResultDims()))) { return failure(); } + return success(); } LogicalResult DispatchOp::verifySymbolUses(SymbolTableCollection &symbolTable) { Operation *op = getOperation(); - auto exportOp = - symbolTable.lookupNearestSymbolFrom( - op, getEntryPoint()); - if (!exportOp) { - // TODO(benvanik): there are a lot of tests that are assuming this is not - // verified. We'll need to go add dummy executables for all of them. Today - // we just bail on the verifier if the symbol isn't found. - // - // Should be: - // return op->emitOpError() << "undefined entry point: " << - // getEntryPoint(); - return success(); - } - - // Verify that the workload parameters captured match the target export. - if (failed(verifyDispatchWorkload(op, exportOp, getWorkload()))) { - return failure(); - } + auto entryPointRefs = getEntryPointRefs(); + if (entryPointRefs.empty()) { + return emitOpError() << "at least one entry point must be defined"; + } + for (auto entryPointAttr : entryPointRefs) { + auto exportOp = + symbolTable.lookupNearestSymbolFrom( + op, entryPointAttr); + if (!exportOp) { + // TODO(benvanik): there are a lot of tests that are assuming this is not + // verified. We'll need to go add dummy executables for all of them. Today + // we just bail on the verifier if the symbol isn't found. + // + // Should be: + // return op->emitOpError() << "undefined entry point: " << + // getEntryPoint(); + return success(); + } - // TODO(benvanik): verify that the target function has matching operands. + // Verify that the workload parameters captured match the target export. + if (failed(verifyDispatchWorkload(op, exportOp, getWorkload()))) { + return failure(); + } + // TODO(benvanik): verify that the target function has matching operands. + } return success(); } diff --git a/compiler/src/iree/compiler/Dialect/Flow/IR/FlowOps.td b/compiler/src/iree/compiler/Dialect/Flow/IR/FlowOps.td index e3d976c359ba..22348702dbab 100644 --- a/compiler/src/iree/compiler/Dialect/Flow/IR/FlowOps.td +++ b/compiler/src/iree/compiler/Dialect/Flow/IR/FlowOps.td @@ -766,7 +766,7 @@ def FLOW_DispatchOp : FLOW_PureOp<"dispatch", [ let arguments = (ins Variadic:$workload, - SymbolRefAttr:$entry_point, + SymbolRefArrayAttr:$entry_points, Variadic:$arguments, FLOW_ShapeDynamicDims:$argument_dims, FLOW_ShapeDynamicDims:$result_dims, @@ -777,7 +777,7 @@ def FLOW_DispatchOp : FLOW_PureOp<"dispatch", [ ); let assemblyFormat = [{ - $entry_point + custom($entry_points) (`[` $workload^ `]`)? `` `(` $arguments `)` attr-dict `:` custom(ref($arguments), @@ -827,9 +827,19 @@ def FLOW_DispatchOp : FLOW_PureOp<"dispatch", [ ]; let extraClassDeclaration = [{ - StringAttr executable(); FunctionType getEntryPointType(); + auto getEntryPointRefs() { + return getEntryPoints().getAsRange(); + } + void forEachEntryPointAttr(std::function fn) { + for (auto entryPointAttr : getEntryPointRefs()) fn(entryPointAttr); + } + + // Returns a human-friendly string name for what is being dispatched. + // May not be unique or a valid reference to an executable. + std::string getEntryPointName(); + // StreamableOpInterface: bool isTransfer() { return false; } @@ -841,6 +851,7 @@ def FLOW_DispatchOp : FLOW_PureOp<"dispatch", [ } }]; + let hasCanonicalizer = 1; let hasVerifier = 1; } diff --git a/compiler/src/iree/compiler/Dialect/Flow/IR/test/dispatch_ops.mlir b/compiler/src/iree/compiler/Dialect/Flow/IR/test/dispatch_ops.mlir index eebae2417c4e..7d0138e028be 100644 --- a/compiler/src/iree/compiler/Dialect/Flow/IR/test/dispatch_ops.mlir +++ b/compiler/src/iree/compiler/Dialect/Flow/IR/test/dispatch_ops.mlir @@ -1,12 +1,12 @@ // RUN: iree-opt --split-input-file %s --verify-diagnostics | FileCheck %s flow.executable @ex0 { + flow.executable.export @dispatch_fn builtin.module { func.func @dispatch_fn(%cst : index, %arg0 : tensor<4xf32>) -> tensor<4xf32> { return %arg0 : tensor<4xf32> } } - flow.executable.export @dispatch_fn } // CHECK-LABEL: @dispatch @@ -18,21 +18,31 @@ func.func @dispatch(%arg0 : tensor<4xf32>) -> tensor<4xf32> { return %0 : tensor<4xf32> } +// ----- + +flow.executable private @ex0 { + flow.executable.export public @dispatch_a + flow.executable.export public @dispatch_b +} + +// CHECK-LABEL: @dispatchWithMultipleRefs +func.func @dispatchWithMultipleRefs(%arg0: tensor<4xf32>) -> tensor<4xf32> { + // CHECK: = flow.dispatch {@ex0::@dispatch_a, @ex0::@dispatch_b}(%arg0) : (tensor<4xf32>) -> tensor<4xf32> + %0 = flow.dispatch {@ex0::@dispatch_a, @ex0::@dispatch_b}(%arg0) : (tensor<4xf32>) -> tensor<4xf32> + return %0 : tensor<4xf32> +} + + // ----- flow.executable private @ex0 { flow.executable.export public @dispatch workgroups(%arg0: index, %arg1: index) -> (index, index, index) { flow.return %arg0, %arg1, %arg0 : index, index, index } - builtin.module { - func.func @dispatch() { - return - } - } } -// CHECK-LABEL: @asyncDispatchWithWorkgroupCount -func.func @asyncDispatchWithWorkgroupCount(%arg0: tensor<4xf32>, %arg1: index) -> tensor<4xf32> { +// CHECK-LABEL: @dispatchWithWorkgroupCount +func.func @dispatchWithWorkgroupCount(%arg0: tensor<4xf32>, %arg1: index) -> tensor<4xf32> { %c1 = arith.constant 1 : index %c2 = arith.constant 2 : index // CHECK: = flow.dispatch @ex0::@dispatch[%c1, %c2](%arg0, %arg1) : (tensor<4xf32>, index) -> tensor<4xf32> @@ -46,14 +56,9 @@ flow.executable private @ex0 { flow.executable.export public @dispatch workgroups(%arg0: index) -> (index, index, index) { flow.return %arg0, %arg0, %arg0 : index, index, index } - builtin.module { - func.func @dispatch() { - return - } - } } -func.func @asyncDispatchWithInvalidWorkload(%arg0: tensor<4xf32>, %arg1: index) -> tensor<4xf32> { +func.func @dispatchWithInvalidWorkload(%arg0: tensor<4xf32>, %arg1: index) -> tensor<4xf32> { %c1 = arith.constant 1 : index %c2 = arith.constant 2 : index // expected-error @+1 {{op workload mismatch; entry point expects 1 arguments but dispatch provides 2}} diff --git a/compiler/src/iree/compiler/Dialect/Flow/Transforms/AnnotateDispatches.cpp b/compiler/src/iree/compiler/Dialect/Flow/Transforms/AnnotateDispatches.cpp index 30e32aad8ee2..851acf528c95 100644 --- a/compiler/src/iree/compiler/Dialect/Flow/Transforms/AnnotateDispatches.cpp +++ b/compiler/src/iree/compiler/Dialect/Flow/Transforms/AnnotateDispatches.cpp @@ -360,10 +360,17 @@ class AnnotateDispatchesPass // new symbol name. for (auto funcLikeOp : getOperation().getOps()) { funcLikeOp->walk([&](IREE::Flow::DispatchOp dispatchOp) { - auto it = entryPointRefReplacements.find(dispatchOp.getEntryPoint()); - if (it != entryPointRefReplacements.end()) { - dispatchOp.setEntryPointAttr(llvm::cast(it->second)); + SmallVector replacementRefs; + for (auto originalRef : dispatchOp.getEntryPointRefs()) { + auto it = entryPointRefReplacements.find(originalRef); + if (it != entryPointRefReplacements.end()) { + replacementRefs.push_back(it->second); + } else { + replacementRefs.push_back(originalRef); + } } + dispatchOp.setEntryPointsAttr( + ArrayAttr::get(dispatchOp.getContext(), replacementRefs)); }); } } diff --git a/compiler/src/iree/compiler/Dialect/Flow/Transforms/DeduplicateExecutables.cpp b/compiler/src/iree/compiler/Dialect/Flow/Transforms/DeduplicateExecutables.cpp index 24b8f4a6faae..80a26a305ed5 100644 --- a/compiler/src/iree/compiler/Dialect/Flow/Transforms/DeduplicateExecutables.cpp +++ b/compiler/src/iree/compiler/Dialect/Flow/Transforms/DeduplicateExecutables.cpp @@ -202,9 +202,20 @@ void replaceEntryPointUses( const DenseMap &replacements) { for (auto funcLikeOp : moduleOp.getOps()) { funcLikeOp->walk([&](DispatchOp dispatchOp) { - auto it = replacements.find(dispatchOp.getEntryPoint()); - if (it != replacements.end()) { - dispatchOp.setEntryPointAttr(llvm::cast(it->second)); + bool didChange = false; + SmallVector newAttrs; + for (auto oldAttr : dispatchOp.getEntryPoints()) { + auto it = replacements.find(oldAttr); + if (it != replacements.end()) { + didChange = true; + newAttrs.push_back(it->second); + } else { + newAttrs.push_back(oldAttr); + } + } + if (didChange) { + dispatchOp.setEntryPointsAttr( + ArrayAttr::get(moduleOp.getContext(), newAttrs)); } }); } diff --git a/compiler/src/iree/compiler/Dialect/Flow/Transforms/DumpDispatchGraph.cpp b/compiler/src/iree/compiler/Dialect/Flow/Transforms/DumpDispatchGraph.cpp index 1603d1584b31..fc99f10073f2 100644 --- a/compiler/src/iree/compiler/Dialect/Flow/Transforms/DumpDispatchGraph.cpp +++ b/compiler/src/iree/compiler/Dialect/Flow/Transforms/DumpDispatchGraph.cpp @@ -377,7 +377,7 @@ class DumpDispatchGraphPass void printDispatchBody(raw_ostream &os, DispatchOp &dispatchOp) { // Find the entry point function from the dispatch entry point symbol // attribute. - auto entryPoint = dispatchOp.getEntryPoint(); + auto entryPoint = *dispatchOp.getEntryPointRefs().begin(); auto executableOp = cast(SymbolTable::lookupNearestSymbolFrom( dispatchOp, entryPoint.getRootReference())); if (!executableOp) @@ -452,7 +452,7 @@ class DumpDispatchGraphPass // Print entry function name, if there is only one entry function, // then the name space and the entry function names are the same, // and we can just print the function name to save space. - auto entryPoint = dispatch.getEntryPoint(); + auto entryPoint = *dispatch.getEntryPointRefs().begin(); auto rootName = entryPoint.getRootReference(); auto leafName = entryPoint.getLeafReference(); if (rootName == leafName) { diff --git a/compiler/src/iree/compiler/Dialect/Flow/Transforms/InjectDispatchTracing.cpp b/compiler/src/iree/compiler/Dialect/Flow/Transforms/InjectDispatchTracing.cpp index c70620b5caaf..77ae538b6ac5 100644 --- a/compiler/src/iree/compiler/Dialect/Flow/Transforms/InjectDispatchTracing.cpp +++ b/compiler/src/iree/compiler/Dialect/Flow/Transforms/InjectDispatchTracing.cpp @@ -37,12 +37,7 @@ class InjectDispatchTracingPass void runOnOperation() override { auto funcOp = getOperation(); for (auto dispatchOp : funcOp.getFunctionBody().getOps()) { - std::string entryPointName = - dispatchOp.getEntryPoint().getRootReference().getValue().str(); - for (FlatSymbolRefAttr nestedRef : - dispatchOp.getEntryPoint().getNestedReferences()) { - entryPointName = (entryPointName + "::" + nestedRef.getValue()).str(); - } + std::string entryPointName = dispatchOp.getEntryPointName(); // Input tensors: OpBuilder builder(dispatchOp); diff --git a/compiler/src/iree/compiler/Dialect/Flow/Transforms/InsertDispatchDebugTargets.cpp b/compiler/src/iree/compiler/Dialect/Flow/Transforms/InsertDispatchDebugTargets.cpp index 6186fd6ae33c..f4d845cd7254 100644 --- a/compiler/src/iree/compiler/Dialect/Flow/Transforms/InsertDispatchDebugTargets.cpp +++ b/compiler/src/iree/compiler/Dialect/Flow/Transforms/InsertDispatchDebugTargets.cpp @@ -169,13 +169,8 @@ struct InsertDebugTargetAtOrdinalPass // Trace on a valid ordinal. if (localTraceOrdinal >= 0 && localTraceOrdinal < dispatchOps.size()) { auto traceTarget = dispatchOps[localTraceOrdinal]; - std::string entryPointName = - traceTarget.getEntryPoint().getRootReference().getValue().str(); - for (FlatSymbolRefAttr nestedRef : - traceTarget.getEntryPoint().getNestedReferences()) { - entryPointName = (entryPointName + "::" + nestedRef.getValue()).str(); - } // Append the ordinal to the trace name. + std::string entryPointName = traceTarget.getEntryPointName(); traceOpWithName(traceTarget, entryPointName + std::string("::") + std::to_string(localTraceOrdinal)); } @@ -226,15 +221,9 @@ struct InsertDebugTargetAtSymbolPass // dispatches. IREE::Flow::DispatchOp breakTarget; funcOp.walk([&](IREE::Flow::DispatchOp dispatchOp) { - std::string entryPointName = - dispatchOp.getEntryPoint().getRootReference().getValue().str(); - for (FlatSymbolRefAttr nestedRef : - dispatchOp.getEntryPoint().getNestedReferences()) { - entryPointName = (entryPointName + "::" + nestedRef.getValue()).str(); - } + std::string entryPointName = dispatchOp.getEntryPointName(); if (traceMatcher.match(entryPointName)) traceOpWithName(dispatchOp, entryPointName); - if (!breakTarget && breakMatcher.match(entryPointName)) breakTarget = dispatchOp; }); diff --git a/compiler/src/iree/compiler/Dialect/Flow/Transforms/test/deduplicate_executables.mlir b/compiler/src/iree/compiler/Dialect/Flow/Transforms/test/deduplicate_executables.mlir index ed27b7f05dcf..254fca8e1223 100644 --- a/compiler/src/iree/compiler/Dialect/Flow/Transforms/test/deduplicate_executables.mlir +++ b/compiler/src/iree/compiler/Dialect/Flow/Transforms/test/deduplicate_executables.mlir @@ -14,7 +14,7 @@ flow.executable @single_executable_ex_0 { func.func @single_executable(%arg0: tensor<4xf32>) -> tensor<4xf32> { %c4 = arith.constant 4 : index // CHECK: %0 = flow.dispatch @single_executable_ex_0::@single_executable_entry_0[%c4](%arg0) : (tensor<4xf32>) -> tensor<4xf32> - %0 = flow.dispatch @single_executable_ex_0::@single_executable_entry_0[%c4] (%arg0) : (tensor<4xf32>) -> tensor<4xf32> + %0 = flow.dispatch @single_executable_ex_0::@single_executable_entry_0[%c4](%arg0) : (tensor<4xf32>) -> tensor<4xf32> return %0 : tensor<4xf32> } @@ -51,15 +51,28 @@ flow.executable @duplicate_executables_ex_2 { } } // CHECK-LABEL: func.func @duplicate_executables -func.func @duplicate_executables(%arg0: tensor<4xf32>) -> tensor<4xf32> { +func.func @duplicate_executables(%arg0: tensor<4xf32>) { %c4 = arith.constant 4 : index - // CHECK: %0 = flow.dispatch @duplicate_executables_ex_0::@duplicate_executables_entry_0[%c4](%arg0) : (tensor<4xf32>) -> tensor<4xf32> - %0 = flow.dispatch @duplicate_executables_ex_0::@duplicate_executables_entry_0[%c4] (%arg0) : (tensor<4xf32>) -> tensor<4xf32> - // CHECK: %1 = flow.dispatch @duplicate_executables_ex_0::@duplicate_executables_entry_0[%c4](%arg0) : (tensor<4xf32>) -> tensor<4xf32> - %1 = flow.dispatch @duplicate_executables_ex_1::@duplicate_executables_entry_1[%c4] (%arg0) : (tensor<4xf32>) -> tensor<4xf32> - // CHECK: %2 = flow.dispatch @duplicate_executables_ex_2::@duplicate_executables_entry_2[%c4](%arg0) : (tensor<4xf32>) -> tensor<4xf32> - %2 = flow.dispatch @duplicate_executables_ex_2::@duplicate_executables_entry_2[%c4] (%arg0) : (tensor<4xf32>) -> tensor<4xf32> - return %0 : tensor<4xf32> + // CHECK: = flow.dispatch @duplicate_executables_ex_0::@duplicate_executables_entry_0[%c4](%arg0) : (tensor<4xf32>) -> tensor<4xf32> + %0 = flow.dispatch @duplicate_executables_ex_0::@duplicate_executables_entry_0[%c4](%arg0) : (tensor<4xf32>) -> tensor<4xf32> + // CHECK: = flow.dispatch @duplicate_executables_ex_0::@duplicate_executables_entry_0[%c4](%arg0) : (tensor<4xf32>) -> tensor<4xf32> + %1 = flow.dispatch @duplicate_executables_ex_1::@duplicate_executables_entry_1[%c4](%arg0) : (tensor<4xf32>) -> tensor<4xf32> + // CHECK: = flow.dispatch @duplicate_executables_ex_2::@duplicate_executables_entry_2[%c4](%arg0) : (tensor<4xf32>) -> tensor<4xf32> + %2 = flow.dispatch @duplicate_executables_ex_2::@duplicate_executables_entry_2[%c4](%arg0) : (tensor<4xf32>) -> tensor<4xf32> + // CHECK: = flow.dispatch {@duplicate_executables_ex_0::@duplicate_executables_entry_0, @duplicate_executables_ex_0::@duplicate_executables_entry_0} + %3 = flow.dispatch {@duplicate_executables_ex_0::@duplicate_executables_entry_0, @duplicate_executables_ex_1::@duplicate_executables_entry_1}[%c4](%arg0) : (tensor<4xf32>) -> tensor<4xf32> + return +} + +// Ensure that symbol renaming is done within initializers. +// CHECK: util.initializer +util.initializer { + // CHECK: %[[CST:.*]] = arith.constant dense<1.000000e+00> + %cst = arith.constant dense<1.000000e+00> : tensor<4xf32> + // CHECK: {{.*}} = flow.dispatch @duplicate_executables_ex_0::@duplicate_executables_entry_0(%[[CST]]) : (tensor<4xf32>) -> tensor<4xf32> + %0 = flow.dispatch @duplicate_executables_ex_1::@duplicate_executables_entry_1(%cst) : (tensor<4xf32>) -> tensor<4xf32> + util.optimization_barrier %0 : tensor<4xf32> + util.initializer.return } // ----- @@ -88,9 +101,9 @@ flow.executable @same_ops_diff_operands_ex_1 { func.func @same_ops_diff_operands(%arg0: tensor<2xi32>, %arg1: tensor<2xi32>) -> tensor<2xi32> { %c4 = arith.constant 4 : index // CHECK: %0 = flow.dispatch @same_ops_diff_operands_ex_0::@entry_0[%c4](%arg0, %arg1) : (tensor<2xi32>, tensor<2xi32>) -> tensor<2xi32> - %0 = flow.dispatch @same_ops_diff_operands_ex_0::@entry_0[%c4] (%arg0, %arg1) : (tensor<2xi32>, tensor<2xi32>) -> tensor<2xi32> + %0 = flow.dispatch @same_ops_diff_operands_ex_0::@entry_0[%c4](%arg0, %arg1) : (tensor<2xi32>, tensor<2xi32>) -> tensor<2xi32> // CHECK: %1 = flow.dispatch @same_ops_diff_operands_ex_1::@entry_1[%c4](%arg0, %arg1) : (tensor<2xi32>, tensor<2xi32>) -> tensor<2xi32> - %1 = flow.dispatch @same_ops_diff_operands_ex_1::@entry_1[%c4] (%arg0, %arg1) : (tensor<2xi32>, tensor<2xi32>) -> tensor<2xi32> + %1 = flow.dispatch @same_ops_diff_operands_ex_1::@entry_1[%c4](%arg0, %arg1) : (tensor<2xi32>, tensor<2xi32>) -> tensor<2xi32> return %0 : tensor<2xi32> } @@ -131,30 +144,16 @@ func.func @multiple_entry_points(%arg0: tensor<4xf32>) -> tensor<4xf32> { // CHECK: %[[C4:.*]] = arith.constant 4 %c4 = arith.constant 4 : index // CHECK: {{.*}} = flow.dispatch @multiple_entry_points_ex_0::@multiple_entry_points_0_entry_0[%[[C4]]](%arg0) : (tensor<4xf32>) -> tensor<4xf32> - %0 = flow.dispatch @multiple_entry_points_ex_0::@multiple_entry_points_0_entry_0[%c4] (%arg0) : (tensor<4xf32>) -> tensor<4xf32> + %0 = flow.dispatch @multiple_entry_points_ex_0::@multiple_entry_points_0_entry_0[%c4](%arg0) : (tensor<4xf32>) -> tensor<4xf32> // CHECK-NEXT: {{.*}} = flow.dispatch @multiple_entry_points_ex_0::@multiple_entry_points_0_entry_1[%[[C4]]](%arg0) : (tensor<4xf32>) -> tensor<4xf32> - %1 = flow.dispatch @multiple_entry_points_ex_0::@multiple_entry_points_0_entry_1[%c4] (%arg0) : (tensor<4xf32>) -> tensor<4xf32> + %1 = flow.dispatch @multiple_entry_points_ex_0::@multiple_entry_points_0_entry_1[%c4](%arg0) : (tensor<4xf32>) -> tensor<4xf32> // CHECK-NEXT: {{.*}} = flow.dispatch @multiple_entry_points_ex_0::@multiple_entry_points_0_entry_0[%[[C4]]](%arg0) : (tensor<4xf32>) -> tensor<4xf32> - %2 = flow.dispatch @multiple_entry_points_ex_1::@multiple_entry_points_1_entry_0[%c4] (%arg0) : (tensor<4xf32>) -> tensor<4xf32> + %2 = flow.dispatch @multiple_entry_points_ex_1::@multiple_entry_points_1_entry_0[%c4](%arg0) : (tensor<4xf32>) -> tensor<4xf32> // CHECK-NEXT: {{.*}} = flow.dispatch @multiple_entry_points_ex_0::@multiple_entry_points_0_entry_1[%[[C4]]](%arg0) : (tensor<4xf32>) -> tensor<4xf32> - %3 = flow.dispatch @multiple_entry_points_ex_1::@multiple_entry_points_1_entry_1[%c4] (%arg0) : (tensor<4xf32>) -> tensor<4xf32> + %3 = flow.dispatch @multiple_entry_points_ex_1::@multiple_entry_points_1_entry_1[%c4](%arg0) : (tensor<4xf32>) -> tensor<4xf32> return %0 : tensor<4xf32> } -// Ensure that symbol renaming is done within initializers. -util.global private @result : tensor<4xf32> -// CHECK: util.initializer -util.initializer { - // CHECK: %[[C4:.*]] = arith.constant 4 - %c4 = arith.constant 4 : index - // CHECK: %[[CST:.*]] = arith.constant dense<1.000000e+00> - %cst = arith.constant dense<1.000000e+00> : tensor<4xf32> - // CHECK: {{.*}} = flow.dispatch @multiple_entry_points_ex_0::@multiple_entry_points_0_entry_1[%[[C4]]](%[[CST]]) : (tensor<4xf32>) -> tensor<4xf32> - %0 = flow.dispatch @multiple_entry_points_ex_1::@multiple_entry_points_1_entry_1[%c4] (%cst) : (tensor<4xf32>) -> tensor<4xf32> - util.global.store %0, @result : tensor<4xf32> - util.initializer.return -} - // ----- // CHECK-LABEL: flow.executable public @different_types_float_ex @@ -181,9 +180,9 @@ flow.executable @different_types_int_ex { func.func @different_types(%arg0: tensor<4xf32>) -> tensor<4xi1> { %c4 = arith.constant 4 : index // CHECK: %0 = flow.dispatch @different_types_float_ex::@different_types_float_entry[%c4](%arg0) : (tensor<4xf32>) -> tensor<4xi1> - %0 = flow.dispatch @different_types_float_ex::@different_types_float_entry[%c4] (%arg0) : (tensor<4xf32>) -> tensor<4xi1> + %0 = flow.dispatch @different_types_float_ex::@different_types_float_entry[%c4](%arg0) : (tensor<4xf32>) -> tensor<4xi1> // CHECK: %1 = flow.dispatch @different_types_int_ex::@different_types_int_entry[%c4](%arg0) : (tensor<4xf32>) -> tensor<4xi1> - %1 = flow.dispatch @different_types_int_ex::@different_types_int_entry[%c4] (%arg0) : (tensor<4xf32>) -> tensor<4xi1> + %1 = flow.dispatch @different_types_int_ex::@different_types_int_entry[%c4](%arg0) : (tensor<4xf32>) -> tensor<4xi1> return %0 : tensor<4xi1> } @@ -239,11 +238,11 @@ flow.executable @nested_ops_ex_2 { func.func @nested_ops(%arg0: tensor<5x6xf32>, %arg1: tensor<5x6xf32>) -> tensor<5x6xf32> { %c4 = arith.constant 4 : index // CHECK: %0 = flow.dispatch @nested_ops_ex_0::@nested_ops_entry_0[%c4](%arg0, %arg1) : (tensor<5x6xf32>, tensor<5x6xf32>) -> tensor<5x6xf32> - %0 = flow.dispatch @nested_ops_ex_0::@nested_ops_entry_0[%c4] (%arg0, %arg1) : (tensor<5x6xf32>, tensor<5x6xf32>) -> tensor<5x6xf32> + %0 = flow.dispatch @nested_ops_ex_0::@nested_ops_entry_0[%c4](%arg0, %arg1) : (tensor<5x6xf32>, tensor<5x6xf32>) -> tensor<5x6xf32> // CHECK: %1 = flow.dispatch @nested_ops_ex_0::@nested_ops_entry_0[%c4](%arg0, %arg1) : (tensor<5x6xf32>, tensor<5x6xf32>) -> tensor<5x6xf32> - %1 = flow.dispatch @nested_ops_ex_0::@nested_ops_entry_0[%c4] (%arg0, %arg1) : (tensor<5x6xf32>, tensor<5x6xf32>) -> tensor<5x6xf32> + %1 = flow.dispatch @nested_ops_ex_0::@nested_ops_entry_0[%c4](%arg0, %arg1) : (tensor<5x6xf32>, tensor<5x6xf32>) -> tensor<5x6xf32> // CHECK: %2 = flow.dispatch @nested_ops_ex_2::@nested_ops_entry_2[%c4](%arg0, %arg1) : (tensor<5x6xf32>, tensor<5x6xf32>) -> tensor<5x6xf32> - %2 = flow.dispatch @nested_ops_ex_2::@nested_ops_entry_2[%c4] (%arg0, %arg1) : (tensor<5x6xf32>, tensor<5x6xf32>) -> tensor<5x6xf32> + %2 = flow.dispatch @nested_ops_ex_2::@nested_ops_entry_2[%c4](%arg0, %arg1) : (tensor<5x6xf32>, tensor<5x6xf32>) -> tensor<5x6xf32> return %0 : tensor<5x6xf32> } diff --git a/compiler/src/iree/compiler/Dialect/Stream/Conversion/FlowToStream/Patterns.cpp b/compiler/src/iree/compiler/Dialect/Stream/Conversion/FlowToStream/Patterns.cpp index 4ae15713d08b..a3c9f44af37e 100644 --- a/compiler/src/iree/compiler/Dialect/Stream/Conversion/FlowToStream/Patterns.cpp +++ b/compiler/src/iree/compiler/Dialect/Stream/Conversion/FlowToStream/Patterns.cpp @@ -571,7 +571,7 @@ struct ConvertDispatchOp : public OpConversionPattern { } auto newOp = rewriter.replaceOpWithNewOp( - op, resultTypes, adaptor.getWorkload(), adaptor.getEntryPoint(), + op, resultTypes, adaptor.getWorkload(), adaptor.getEntryPointsAttr(), dispatchOperands, dispatchOperandSizes, dispatchOperandOffsets, dispatchOperandEnds, dispatchOperandLengths, resultSizes, adaptor.getTiedOperandsAttr(), getAffinityFor(op)); diff --git a/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOpFolders.cpp b/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOpFolders.cpp index 3951a0377bcc..31783e65737e 100644 --- a/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOpFolders.cpp +++ b/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOpFolders.cpp @@ -1888,10 +1888,30 @@ void AsyncStoreOp::getCanonicalizationPatterns(RewritePatternSet &results, // stream.async.dispatch //===----------------------------------------------------------------------===// +namespace { + +struct DeduplicateAsyncDispatchEntryRefs final + : public OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + LogicalResult matchAndRewrite(AsyncDispatchOp dispatchOp, + PatternRewriter &rewriter) const override { + auto originalAttr = dispatchOp.getEntryPointsAttr(); + auto newAttr = deduplicateArrayElements(originalAttr); + if (newAttr == originalAttr) + return failure(); + rewriter.updateRootInPlace( + dispatchOp, [&]() { dispatchOp.setEntryPointsAttr(newAttr); }); + return success(); + } +}; + +} // namespace + void AsyncDispatchOp::getCanonicalizationPatterns(RewritePatternSet &results, MLIRContext *context) { - // TODO(benvanik): nothing? maybe tied type/lifetime updates? + // TODO(benvanik):maybe tied type/lifetime updates? results.insert>(context); + results.insert(context); } //===----------------------------------------------------------------------===// @@ -2316,8 +2336,28 @@ void CmdCollectiveOp::getCanonicalizationPatterns(RewritePatternSet &results, // stream.cmd.dispatch //===----------------------------------------------------------------------===// +namespace { + +struct DeduplicateCmdDispatchEntryRefs final + : public OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + LogicalResult matchAndRewrite(CmdDispatchOp dispatchOp, + PatternRewriter &rewriter) const override { + auto originalAttr = dispatchOp.getEntryPointsAttr(); + auto newAttr = deduplicateArrayElements(originalAttr); + if (newAttr == originalAttr) + return failure(); + rewriter.updateRootInPlace( + dispatchOp, [&]() { dispatchOp.setEntryPointsAttr(newAttr); }); + return success(); + } +}; + +} // namespace + void CmdDispatchOp::getCanonicalizationPatterns(RewritePatternSet &results, MLIRContext *context) { + results.insert(context); results.insert>(context); } diff --git a/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOps.cpp b/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOps.cpp index a294923d2133..b3dfc6292cd8 100644 --- a/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOps.cpp +++ b/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOps.cpp @@ -274,6 +274,44 @@ static IREE::Util::ValueAccess computeValueAccess(Value rootValue) { return access; } +//===----------------------------------------------------------------------===// +// custom($entry_points) +//===----------------------------------------------------------------------===// + +static ParseResult parseDispatchEntryPoints(OpAsmParser &parser, + ArrayAttr &entryPointAttrsArray) { + SmallVector entryPointAttrs; + if (succeeded(parser.parseOptionalLBrace())) { + do { + SymbolRefAttr entryPointAttr; + if (failed(parser.parseAttribute(entryPointAttr))) + return failure(); + entryPointAttrs.push_back(entryPointAttr); + } while (succeeded(parser.parseOptionalComma())); + if (failed(parser.parseRBrace())) + return failure(); + } else { + SymbolRefAttr entryPointAttr; + if (failed(parser.parseAttribute(entryPointAttr))) + return failure(); + entryPointAttrs.push_back(entryPointAttr); + } + entryPointAttrsArray = parser.getBuilder().getArrayAttr(entryPointAttrs); + return success(); +} + +static void printDispatchEntryPoints(OpAsmPrinter &p, Operation *op, + ArrayAttr entryPointAttrs) { + if (entryPointAttrs.size() == 1) { + p.printAttribute(entryPointAttrs.getValue().front()); + } else { + p << '{'; + llvm::interleaveComma(entryPointAttrs, p.getStream(), + [&](Attribute attr) { p.printAttribute(attr); }); + p << '}'; + } +} + //===----------------------------------------------------------------------===// // custom( // $resources, type($resources), $resource_sizes, @@ -1790,26 +1828,32 @@ LogicalResult AsyncDispatchOp::verify() { LogicalResult AsyncDispatchOp::verifySymbolUses(SymbolTableCollection &symbolTable) { Operation *op = getOperation(); - auto exportOp = - symbolTable.lookupNearestSymbolFrom( - op, getEntryPoint()); - if (!exportOp) { - // TODO(benvanik): there are a lot of tests that are assuming this is not - // verified. We'll need to go add dummy executables for all of them. Today - // we just bail on the verifier if the symbol isn't found. - // - // Should be: - // return op->emitOpError() << "undefined entry point: " << entry_point(); - return success(); - } - - // Verify that the workload parameters captured match the target export. - if (failed(verifyDispatchWorkload(op, exportOp, getWorkload()))) { - return failure(); + auto entryPointRefs = getEntryPointRefs(); + if (entryPointRefs.empty()) { + return emitOpError() << "at least one entry point must be defined"; } + for (auto entryPointAttr : entryPointRefs) { + auto exportOp = + symbolTable.lookupNearestSymbolFrom( + op, entryPointAttr); + if (!exportOp) { + // TODO(benvanik): there are a lot of tests that are assuming this is not + // verified. We'll need to go add dummy executables for all of them. Today + // we just bail on the verifier if the symbol isn't found. + // + // Should be: + // return op->emitOpError() << "undefined entry point: " << + // entry_point(); + return success(); + } - // TODO(benvanik): verify that the target function has matching operands. + // Verify that the workload parameters captured match the target export. + if (failed(verifyDispatchWorkload(op, exportOp, getWorkload()))) { + return failure(); + } + // TODO(benvanik): verify that the target function has matching operands. + } return success(); } @@ -2489,40 +2533,6 @@ CmdDispatchOp::verifySymbolUses(SymbolTableCollection &symbolTable) { return success(); } -static ParseResult parseDispatchEntryPoints(OpAsmParser &parser, - ArrayAttr &entryPointAttrsArray) { - SmallVector entryPointAttrs; - if (succeeded(parser.parseOptionalLBrace())) { - do { - SymbolRefAttr entryPointAttr; - if (failed(parser.parseAttribute(entryPointAttr))) - return failure(); - entryPointAttrs.push_back(entryPointAttr); - } while (succeeded(parser.parseOptionalComma())); - if (failed(parser.parseRBrace())) - return failure(); - } else { - SymbolRefAttr entryPointAttr; - if (failed(parser.parseAttribute(entryPointAttr))) - return failure(); - entryPointAttrs.push_back(entryPointAttr); - } - entryPointAttrsArray = parser.getBuilder().getArrayAttr(entryPointAttrs); - return success(); -} - -static void printDispatchEntryPoints(OpAsmPrinter &p, Operation *op, - ArrayAttr entryPointAttrs) { - if (entryPointAttrs.size() == 1) { - p.printAttribute(entryPointAttrs.getValue().front()); - } else { - p << '{'; - llvm::interleaveComma(entryPointAttrs, p.getStream(), - [&](Attribute attr) { p.printAttribute(attr); }); - p << '}'; - } -} - static ParseResult parseDispatchResources( OpAsmParser &parser, SmallVectorImpl &resources, diff --git a/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOps.td b/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOps.td index 0d5271860a6c..988cf86a1f63 100644 --- a/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOps.td +++ b/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOps.td @@ -2057,7 +2057,7 @@ def Stream_AsyncDispatchOp : Stream_Op<"async.dispatch", [ let arguments = (ins Variadic:$workload, - SymbolRefAttr:$entry_point, + SymbolRefArrayAttr:$entry_points, Variadic($entry_points) (`[` $workload^ `]`)? `` custom($resource_operands, $resource_operand_offsets, @@ -2089,6 +2089,13 @@ def Stream_AsyncDispatchOp : Stream_Op<"async.dispatch", [ }]; let extraClassDeclaration = [{ + auto getEntryPointRefs() { + return getEntryPoints().getAsRange(); + } + void forEachEntryPointAttr(std::function fn) { + for (auto entryPointAttr : getEntryPointRefs()) fn(entryPointAttr); + } + Value getOperandSize(unsigned idx) { return findValueSizeInList(idx, getOperands(), getResourceOperandSizes()); } diff --git a/compiler/src/iree/compiler/Dialect/Stream/Transforms/MaterializeBuiltins.cpp b/compiler/src/iree/compiler/Dialect/Stream/Transforms/MaterializeBuiltins.cpp index 8d5d69424055..caf865848d64 100644 --- a/compiler/src/iree/compiler/Dialect/Stream/Transforms/MaterializeBuiltins.cpp +++ b/compiler/src/iree/compiler/Dialect/Stream/Transforms/MaterializeBuiltins.cpp @@ -215,9 +215,9 @@ static LogicalResult replaceBuiltinSplatOp(IREE::Stream::AsyncSplatOp splatOp, OpBuilder builder(splatOp); auto dispatchOp = builder.create( loc, resultTypes, workload, - SymbolRefAttr::get( + builder.getArrayAttr({SymbolRefAttr::get( builder.getStringAttr(builtinName), - FlatSymbolRefAttr::get(builder.getContext(), builtinName)), + FlatSymbolRefAttr::get(builder.getContext(), builtinName))}), operands, operandSizes, operandOffsets, operandEnds, operandLengths, resultSizes, builder.getIndexArrayAttr(tiedOperands), splatOp.getAffinityAttr()); @@ -311,9 +311,9 @@ static LogicalResult replaceBuiltinFillOp(IREE::Stream::AsyncFillOp fillOp, OpBuilder builder(fillOp); auto dispatchOp = builder.create( loc, resultTypes, workload, - SymbolRefAttr::get( + builder.getArrayAttr({SymbolRefAttr::get( builder.getStringAttr(builtinName), - FlatSymbolRefAttr::get(builder.getContext(), builtinName)), + FlatSymbolRefAttr::get(builder.getContext(), builtinName))}), operands, operandSizes, operandOffsets, operandEnds, operandLengths, resultSizes, builder.getIndexArrayAttr(tiedOperands), fillOp.getAffinityAttr()); diff --git a/compiler/src/iree/compiler/Dialect/Stream/Transforms/ScheduleAllocation.cpp b/compiler/src/iree/compiler/Dialect/Stream/Transforms/ScheduleAllocation.cpp index 14af630429c0..367f4aa8df4d 100644 --- a/compiler/src/iree/compiler/Dialect/Stream/Transforms/ScheduleAllocation.cpp +++ b/compiler/src/iree/compiler/Dialect/Stream/Transforms/ScheduleAllocation.cpp @@ -765,10 +765,9 @@ static LogicalResult applyAsyncDispatchOp(IREE::Stream::AsyncDispatchOp asyncOp, } auto newOp = builder.create( - asyncOp.getLoc(), asyncOp.getWorkload(), - builder.getArrayAttr({asyncOp.getEntryPoint()}), newOperands, - newResources, newResourceSizes, newResourceOffsets, newResourceLengths, - builder.getArrayAttr(newResourceAccesses)); + asyncOp.getLoc(), asyncOp.getWorkload(), asyncOp.getEntryPointsAttr(), + newOperands, newResources, newResourceSizes, newResourceOffsets, + newResourceLengths, builder.getArrayAttr(newResourceAccesses)); newOp->setDialectAttrs(asyncOp->getDialectAttrs()); asyncOp.erase(); return success(); diff --git a/compiler/src/iree/compiler/Dialect/Util/IR/UtilOps.cpp b/compiler/src/iree/compiler/Dialect/Util/IR/UtilOps.cpp index 5e4f0ead8db0..f5cefd253942 100644 --- a/compiler/src/iree/compiler/Dialect/Util/IR/UtilOps.cpp +++ b/compiler/src/iree/compiler/Dialect/Util/IR/UtilOps.cpp @@ -60,6 +60,13 @@ Value buildIfElseTree( // Utils //===----------------------------------------------------------------------===// +ArrayAttr deduplicateArrayElements(ArrayAttr arrayAttr) { + SetVector attrsSet(arrayAttr.begin(), arrayAttr.end()); + if (attrsSet.size() == arrayAttr.size()) + return arrayAttr; + return ArrayAttr::get(arrayAttr.getContext(), attrsSet.takeVector()); +} + Value findValueSizeInList(unsigned index, ValueRange values, ValueRange sizes) { assert(values[index].getType().isa() && "must be a size-aware type to get dims"); diff --git a/compiler/src/iree/compiler/Dialect/Util/IR/UtilOps.h b/compiler/src/iree/compiler/Dialect/Util/IR/UtilOps.h index b89675f9b640..170f7eb6a6a7 100644 --- a/compiler/src/iree/compiler/Dialect/Util/IR/UtilOps.h +++ b/compiler/src/iree/compiler/Dialect/Util/IR/UtilOps.h @@ -48,6 +48,9 @@ Value buildIfElseTree( // Utils //===----------------------------------------------------------------------===// +// Removes duplicate attributes in the array (if any). +ArrayAttr deduplicateArrayElements(ArrayAttr arrayAttr); + // Returns the dynamic size of the value at |index|. Value findValueSizeInList(unsigned index, ValueRange values, ValueRange sizes);