| // Copyright 2019 The IREE Authors |
| // |
| // Licensed 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 |
| |
| #include "iree/compiler/Dialect/HAL/IR/HALOps.h" |
| |
| #include "iree/compiler/Dialect/HAL/IR/HALTypes.h" |
| #include "iree/compiler/Dialect/Util/IR/UtilTypes.h" |
| #include "llvm/ADT/STLExtras.h" |
| #include "mlir/Dialect/Arith/IR/Arith.h" |
| #include "mlir/Dialect/SCF/IR/SCF.h" |
| #include "mlir/IR/Attributes.h" |
| #include "mlir/IR/Builders.h" |
| #include "mlir/IR/BuiltinTypes.h" |
| #include "mlir/IR/IRMapping.h" |
| #include "mlir/IR/OpImplementation.h" |
| #include "mlir/IR/SymbolTable.h" |
| #include "mlir/IR/TypeUtilities.h" |
| #include "mlir/Interfaces/FunctionImplementation.h" |
| #include "mlir/Interfaces/InferIntRangeInterface.h" |
| #include "mlir/Interfaces/InferTypeOpInterface.h" |
| |
| namespace mlir::iree_compiler::IREE::HAL { |
| |
| namespace { |
| |
| // We aribtrarily say that unbounded dimensions in a torch program cannot |
| // exceed 53bits, making the maximum safe dimension 9007199254740991. The |
| // astute reader will note that this is also the maximum safe value in |
| // JavaScript, which also "happens" to be the largest mantissa value in a |
| // 64bit double. We need a maximum and in the absence of a better choice, |
| // with this one we are at least in good company. This limit is also used |
| // in the frontends. |
| static constexpr uint64_t MAX_DIM_VALUE = (static_cast<uint64_t>(1) << 53) - 1; |
| |
| // Similarly we use a very conservative maximum rank value for specifying |
| // ranges of runtime rank resolution functions. Various frameworks have hard |
| // and practical limits ranging from 32 (numpy) to hundreds. At the time of |
| // writing, PyTorch throws weird errors if trying to print a tensor with a rank |
| // greater than 992. We really just want a smallish integer value to bound |
| // arithmetic, so we use an arbitrary maximum. |
| static constexpr uint64_t MAX_RANK_VALUE = 4096; |
| |
| } // namespace |
| |
| //===----------------------------------------------------------------------===// |
| // custom<DescriptorType>($descriptor_type) |
| //===----------------------------------------------------------------------===// |
| |
| // Custom parser/printer to omit the wrapping `<` and `>` unlike autogenerated |
| // attribute parser/printer. |
| static ParseResult parseDescriptorType(OpAsmParser &parser, |
| DescriptorTypeAttr &dtAttr) { |
| StringRef enumKeyword; |
| if (failed(parser.parseKeyword(&enumKeyword))) |
| return failure(); |
| std::optional<DescriptorType> maybeEnum = |
| symbolizeDescriptorType(enumKeyword); |
| if (!maybeEnum) |
| return failure(); |
| dtAttr = DescriptorTypeAttr::get(parser.getContext(), *maybeEnum); |
| return success(); |
| } |
| |
| static void printDescriptorType(OpAsmPrinter &p, Operation *, |
| DescriptorTypeAttr dtAttr) { |
| p << stringifyDescriptorType(dtAttr.getValue()); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // custom<PipelineBindings>($binding_ordinals, |
| // $binding_buffers, |
| // type($binding_buffers), |
| // $binding_offsets, |
| // $binding_lengths) |
| //===----------------------------------------------------------------------===// |
| |
| static ParseResult parsePipelineBindings( |
| OpAsmParser &parser, |
| SmallVectorImpl<OpAsmParser::UnresolvedOperand> &ordinals, |
| SmallVectorImpl<OpAsmParser::UnresolvedOperand> &buffers, |
| SmallVectorImpl<Type> &bufferTypes, |
| SmallVectorImpl<OpAsmParser::UnresolvedOperand> &bufferOffsets, |
| SmallVectorImpl<OpAsmParser::UnresolvedOperand> &bufferLengths) { |
| do { |
| OpAsmParser::UnresolvedOperand ordinal; |
| OpAsmParser::UnresolvedOperand buffer; |
| Type bufferType; |
| OpAsmParser::UnresolvedOperand bufferOffset; |
| OpAsmParser::UnresolvedOperand bufferLength; |
| if (failed(parser.parseOperand(ordinal)) || failed(parser.parseEqual()) || |
| failed(parser.parseLParen()) || failed(parser.parseOperand(buffer)) || |
| failed(parser.parseColonType(bufferType)) || |
| failed(parser.parseRParen()) || failed(parser.parseLSquare()) || |
| failed(parser.parseOperand(bufferOffset)) || |
| failed(parser.parseComma()) || |
| failed(parser.parseOperand(bufferLength)) || |
| failed(parser.parseRSquare())) { |
| return failure(); |
| } |
| ordinals.push_back(ordinal); |
| buffers.push_back(buffer); |
| bufferTypes.push_back(bufferType); |
| bufferOffsets.push_back(bufferOffset); |
| bufferLengths.push_back(bufferLength); |
| } while (succeeded(parser.parseOptionalComma())); |
| return success(); |
| } |
| |
| static void printPipelineBindings(OpAsmPrinter &p, Operation *op, |
| ValueRange ordinals, ValueRange buffers, |
| TypeRange bufferTypes, |
| ValueRange bufferOffsets, |
| ValueRange bufferLengths) { |
| llvm::interleaveComma(llvm::zip_equal(ordinals, buffers, bufferTypes, |
| bufferOffsets, bufferLengths), |
| p, |
| [&](std::tuple<Value, Value, Type, Value, Value> it) { |
| p.printNewline(); |
| p << " "; |
| p.printOperand(std::get<0>(it)); |
| p << " = ("; |
| p.printOperand(std::get<1>(it)); |
| p << " : "; |
| p.printType(std::get<2>(it)); |
| p << ")["; |
| p.printOperand(std::get<3>(it)); |
| p << ", "; |
| p.printOperand(std::get<4>(it)); |
| p << "]"; |
| }); |
| p.printNewline(); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // custom<Bindings>($binding_buffers, |
| // type($binding_buffers), |
| // $binding_offsets, |
| // $binding_lengths) |
| //===----------------------------------------------------------------------===// |
| |
| static ParseResult |
| parseBindings(OpAsmParser &parser, |
| SmallVectorImpl<OpAsmParser::UnresolvedOperand> &buffers, |
| SmallVectorImpl<Type> &bufferTypes, |
| SmallVectorImpl<OpAsmParser::UnresolvedOperand> &bufferOffsets, |
| SmallVectorImpl<OpAsmParser::UnresolvedOperand> &bufferLengths) { |
| do { |
| OpAsmParser::UnresolvedOperand buffer; |
| Type bufferType; |
| OpAsmParser::UnresolvedOperand bufferOffset; |
| OpAsmParser::UnresolvedOperand bufferLength; |
| if (failed(parser.parseLParen()) || failed(parser.parseOperand(buffer)) || |
| failed(parser.parseColonType(bufferType)) || |
| failed(parser.parseRParen()) || failed(parser.parseLSquare()) || |
| failed(parser.parseOperand(bufferOffset)) || |
| failed(parser.parseComma()) || |
| failed(parser.parseOperand(bufferLength)) || |
| failed(parser.parseRSquare())) { |
| return failure(); |
| } |
| buffers.push_back(buffer); |
| bufferTypes.push_back(bufferType); |
| bufferOffsets.push_back(bufferOffset); |
| bufferLengths.push_back(bufferLength); |
| } while (succeeded(parser.parseOptionalComma())); |
| return success(); |
| } |
| |
| static void printBindings(OpAsmPrinter &p, Operation *op, ValueRange buffers, |
| TypeRange bufferTypes, ValueRange bufferOffsets, |
| ValueRange bufferLengths) { |
| llvm::interleaveComma( |
| llvm::zip_equal(buffers, bufferTypes, bufferOffsets, bufferLengths), p, |
| [&](std::tuple<Value, Type, Value, Value> it) { |
| p.printNewline(); |
| p << " ("; |
| p.printOperand(std::get<0>(it)); |
| p << " : "; |
| p.printType(std::get<1>(it)); |
| p << ")["; |
| p.printOperand(std::get<2>(it)); |
| p << ", "; |
| p.printOperand(std::get<3>(it)); |
| p << "]"; |
| }); |
| p.printNewline(); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // custom<BindingTable>($binding_buffers, |
| // type($binding_buffers), |
| // $binding_offsets, |
| // $binding_lengths) |
| //===----------------------------------------------------------------------===// |
| |
| static ParseResult parseBindingTable( |
| OpAsmParser &parser, |
| SmallVectorImpl<OpAsmParser::UnresolvedOperand> &buffers, |
| SmallVectorImpl<Type> &bufferTypes, |
| SmallVectorImpl<OpAsmParser::UnresolvedOperand> &bufferOffsets, |
| SmallVectorImpl<OpAsmParser::UnresolvedOperand> &bufferLengths) { |
| do { |
| OpAsmParser::UnresolvedOperand buffer; |
| Type bufferType; |
| OpAsmParser::UnresolvedOperand bufferOffset; |
| OpAsmParser::UnresolvedOperand bufferLength; |
| if (failed(parser.parseLParen()) || failed(parser.parseOperand(buffer)) || |
| failed(parser.parseColonType(bufferType)) || |
| failed(parser.parseRParen()) || failed(parser.parseLSquare()) || |
| failed(parser.parseOperand(bufferOffset)) || |
| failed(parser.parseComma()) || |
| failed(parser.parseOperand(bufferLength)) || |
| failed(parser.parseRSquare())) { |
| return failure(); |
| } |
| buffers.push_back(buffer); |
| bufferTypes.push_back(bufferType); |
| bufferOffsets.push_back(bufferOffset); |
| bufferLengths.push_back(bufferLength); |
| } while (succeeded(parser.parseOptionalComma())); |
| return success(); |
| } |
| |
| static void printBindingTable(OpAsmPrinter &p, Operation *op, |
| ValueRange buffers, TypeRange bufferTypes, |
| ValueRange bufferOffsets, |
| ValueRange bufferLengths) { |
| llvm::interleaveComma( |
| llvm::zip_equal(buffers, bufferTypes, bufferOffsets, bufferLengths), p, |
| [&](std::tuple<Value, Type, Value, Value> it) { |
| p.printNewline(); |
| p << " "; |
| p << "("; |
| p.printOperand(std::get<0>(it)); |
| p << " : "; |
| p.printType(std::get<1>(it)); |
| p << ")["; |
| p.printOperand(std::get<2>(it)); |
| p << ", "; |
| p.printOperand(std::get<3>(it)); |
| p << "]"; |
| }); |
| p.printNewline(); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // custom<TargetConditionRegion>($body) |
| //===----------------------------------------------------------------------===// |
| |
| static FunctionType getTargetConditionRegionType(MLIRContext *context) { |
| return FunctionType::get(context, |
| { |
| IREE::HAL::DeviceType::get(context), |
| }, |
| { |
| IntegerType::get(context, 1), |
| }); |
| } |
| |
| static LogicalResult verifyTargetConditionRegion(Operation *op, |
| Region ®ion) { |
| // Ignore if empty. |
| if (region.empty()) { |
| return success(); |
| } |
| |
| // Verify region takes a !hal.device. |
| if (region.getNumArguments() != 1 || |
| !isa<IREE::HAL::DeviceType>(region.getArgumentTypes().front())) { |
| return op->emitOpError() |
| << "target condition region must take a !hal.device"; |
| } |
| |
| // Verify i1 return. |
| for (auto returnOp : region.getOps<IREE::HAL::ReturnOp>()) { |
| if (returnOp.getNumOperands() != 1) { |
| return returnOp.emitOpError() |
| << "target condition region must return a single i1 result"; |
| } |
| for (auto returnType : returnOp.getOperandTypes()) { |
| if (!returnType.isInteger(1)) { |
| return returnOp.emitOpError() |
| << "target condition region must return a single i1 result"; |
| } |
| } |
| } |
| |
| return success(); |
| } |
| |
| static ParseResult parseTargetConditionRegion(OpAsmParser &parser, |
| Region &body) { |
| SmallVector<OpAsmParser::Argument> args; |
| if (failed(parser.parseArgumentList(args, AsmParser::Delimiter::Paren, |
| /*allowType=*/true, |
| /*allowAttrs=*/true))) { |
| return failure(); |
| } |
| |
| SmallVector<Type> returnTypes; |
| if (failed(parser.parseArrowTypeList(returnTypes))) { |
| return failure(); |
| } |
| if (returnTypes.size() != 1 || |
| !llvm::all_of(returnTypes, [](Type type) { return type.isInteger(1); })) { |
| return parser.emitError(parser.getCurrentLocation()) |
| << "target condition region must return one i1"; |
| } |
| |
| return parser.parseRegion(body, args, /*enableNameShadowing=*/false); |
| } |
| |
| static void printTargetConditionRegion(OpAsmPrinter &p, Operation *op, |
| Region &body) { |
| if (body.empty()) |
| return; |
| p << "("; |
| llvm::interleaveComma(body.getArguments(), p, |
| [&](BlockArgument arg) { p.printRegionArgument(arg); }); |
| p << ")"; |
| p.printArrowTypeList(TypeRange{IntegerType::get(body.getContext(), 1)}); |
| p << " "; |
| p.printRegion(body, /*printEntryBlockArgs=*/false, |
| /*printBlockTerminators=*/true); |
| } |
| |
| static ParseResult parseTargetConditionObjects( |
| OpAsmParser &parser, ArrayAttr &targetsAttr, ArrayAttr &targetOrdinalsAttr, |
| ArrayAttr &targetObjectsAttr, |
| SmallVector<std::unique_ptr<Region>, 2> &targetRegions) { |
| SmallVector<Attribute> targetsAttrs; |
| SmallVector<Attribute> targetOrdinalsAttrs; |
| SmallVector<Attribute> targetObjectsAttrs; |
| do { |
| // #hal.executable.target<...> |
| Attribute targetAttr; |
| if (failed(parser.parseAttribute(targetAttr))) |
| return failure(); |
| targetsAttrs.push_back(targetAttr); |
| |
| // if(...) -> i1 { ... } |
| auto region = std::make_unique<Region>(); |
| if (succeeded(parser.parseOptionalKeyword("if"))) { |
| if (failed(parseTargetConditionRegion(parser, *region))) |
| return failure(); |
| } |
| targetRegions.push_back(std::move(region)); |
| |
| // ordinal(#) |
| Attribute targetOrdinalAttr; |
| if (failed(parser.parseKeyword("ordinal")) || |
| failed(parser.parseLParen()) || |
| failed(parser.parseAttribute(targetOrdinalAttr, |
| IndexType::get(parser.getContext()))) || |
| failed(parser.parseRParen())) |
| return failure(); |
| targetOrdinalsAttrs.push_back(targetOrdinalAttr); |
| |
| // = [#hal.executable.object<...>, ...] |
| ArrayAttr targetObjectsAttr; |
| if (failed(parser.parseEqual()) || |
| failed(parser.parseAttribute(targetObjectsAttr))) |
| return failure(); |
| targetObjectsAttrs.push_back(targetObjectsAttr); |
| } while (succeeded(parser.parseOptionalComma())); |
| targetsAttr = ArrayAttr::get(parser.getContext(), targetsAttrs); |
| targetOrdinalsAttr = ArrayAttr::get(parser.getContext(), targetOrdinalsAttrs); |
| targetObjectsAttr = ArrayAttr::get(parser.getContext(), targetObjectsAttrs); |
| return success(); |
| } |
| |
| static void printTargetConditionObjects(OpAsmPrinter &p, Operation *op, |
| ArrayAttr targetsAttr, |
| ArrayAttr targetOrdinalsAttr, |
| ArrayAttr targetObjectsAttr, |
| MutableArrayRef<Region> targetRegions) { |
| p.increaseIndent(); |
| p.printNewline(); |
| |
| llvm::interleave( |
| llvm::zip_equal(targetsAttr, targetOrdinalsAttr, targetObjectsAttr, |
| targetRegions), |
| [&](auto it) { |
| auto &[targetAttr, targetOrdinalAttr, targetObjectsAttr, targetRegion] = |
| it; |
| p.printAttribute(targetAttr); |
| if (!targetRegion.empty()) { |
| p << " if"; |
| printTargetConditionRegion(p, op, targetRegion); |
| } |
| p << " ordinal("; |
| p.printAttributeWithoutType(targetOrdinalAttr); |
| p << ")"; |
| p << " = "; |
| p.printAttribute(targetObjectsAttr); |
| }, |
| [&]() { |
| p << ","; |
| p.printNewline(); |
| }); |
| |
| p.decreaseIndent(); |
| p.printNewline(); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // custom<WorkgroupCountRegion>($body) |
| //===----------------------------------------------------------------------===// |
| |
| static ParseResult parseWorkgroupCountRegion(OpAsmParser &parser, |
| Region &body) { |
| SmallVector<OpAsmParser::Argument> args; |
| if (failed(parser.parseArgumentList(args, AsmParser::Delimiter::Paren, |
| /*allowType=*/true, |
| /*allowAttrs=*/true))) { |
| return failure(); |
| } |
| |
| // Return types must be 3 dimensions (workgroup count XYZ). |
| SmallVector<Type> returnTypes; |
| if (failed(parser.parseArrowTypeList(returnTypes))) { |
| return failure(); |
| } |
| if (returnTypes.size() != 3 || |
| !llvm::all_of(returnTypes, [](Type type) { return type.isIndex(); })) { |
| return parser.emitError(parser.getCurrentLocation()) |
| << "workgroup count region must return the XYZ dimension counts"; |
| } |
| |
| // Parse region contents. |
| if (failed(parser.parseRegion(body, args, /*enableNameShadowing=*/false))) { |
| return failure(); |
| } |
| |
| // Verify the return types match. |
| for (auto returnOp : body.getOps<IREE::HAL::ReturnOp>()) { |
| for (auto [resultType, returnType] : |
| llvm::zip_equal(returnTypes, returnOp.getOperandTypes())) { |
| if (resultType != returnType) { |
| return returnOp.emitOpError() |
| << "operands do not match expected region return types"; |
| } |
| } |
| } |
| |
| return success(); |
| } |
| |
| static void printWorkgroupCountRegion(OpAsmPrinter &p, Operation *op, |
| Region &body) { |
| if (body.empty()) |
| return; |
| p << "("; |
| llvm::interleaveComma(body.getArguments(), p, |
| [&](BlockArgument arg) { p.printRegionArgument(arg); }); |
| p << ")"; |
| Type indexType = IndexType::get(body.getContext()); |
| p.printArrowTypeList(TypeRange{indexType, indexType, indexType}); |
| p << " "; |
| p.printRegion(body, /*printEntryBlockArgs=*/false, |
| /*printBlockTerminators=*/true); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.ex.* |
| //===----------------------------------------------------------------------===// |
| |
| void ExFileFromMemoryOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| setNameFn(getResult(), "memory_file"); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.return |
| //===----------------------------------------------------------------------===// |
| |
| LogicalResult ReturnOp::verify() { |
| ReturnOp op = *this; |
| |
| auto parentFuncOp = |
| dyn_cast_or_null<mlir::FunctionOpInterface>(op->getParentOp()); |
| if (parentFuncOp) { |
| auto expectedTypes = parentFuncOp.getResultTypes(); |
| if (op.getNumOperands() != expectedTypes.size()) { |
| return op.emitOpError() << "return must have the same number of operands " |
| "as the parent result signature (have " |
| << op.getNumOperands() << ", expected " |
| << expectedTypes.size() << ")"; |
| } |
| for (auto &&[index, values] : |
| llvm::enumerate(llvm::zip_equal(op.getOperands(), expectedTypes))) { |
| auto [operand, expectedType] = values; |
| if (operand.getType() != expectedType) { |
| return op.emitOpError() |
| << "parent expected result " << index << " to be " |
| << expectedType << " but returning " << operand.getType(); |
| } |
| } |
| } |
| |
| return success(); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.tensor.import/export |
| //===----------------------------------------------------------------------===// |
| |
| void TensorImportOp::build(OpBuilder &builder, OperationState &result, |
| Type resultType, Value source, |
| TypeAttr targetEncoding, StringAttr name, |
| Attribute affinity) { |
| build(builder, result, resultType, source, targetEncoding, |
| /*waitFence=*/Value{}, name, affinity); |
| } |
| |
| void TensorImportOp::build(OpBuilder &builder, OperationState &result, |
| Type resultType, Value source, |
| TypeAttr targetEncoding, Value waitFence, |
| StringAttr name, Attribute affinity) { |
| auto shapedType = llvm::cast<ShapedType>(resultType); |
| assert((isa<IREE::HAL::BufferViewType>(source.getType()) || |
| shapedType.hasStaticShape()) && |
| "can only use this constructor for buffer views when shape " |
| "information is required"); |
| SmallVector<Value> dynamicDims; |
| for (int64_t i = 0; i < shapedType.getRank(); ++i) { |
| if (!shapedType.isDynamicDim(i)) |
| continue; |
| dynamicDims.push_back(builder.createOrFold<IREE::HAL::BufferViewDimOp>( |
| result.location, builder.getIndexType(), source, |
| builder.getIndexAttr(i))); |
| } |
| build(builder, result, resultType, source, targetEncoding, dynamicDims, |
| waitFence, name, affinity); |
| } |
| |
| static LogicalResult verifyTypeStorageCompatibility(Operation *op, |
| Type encodingType, |
| Type storageType) { |
| if (encodingType == storageType) |
| return success(); |
| auto encodingShapedType = llvm::dyn_cast<ShapedType>(encodingType); |
| auto storageShapedType = llvm::dyn_cast<ShapedType>(storageType); |
| if (!encodingShapedType || !storageShapedType) |
| return success(); |
| |
| if (IREE::Util::getRoundedElementByteWidth( |
| encodingShapedType.getElementType()) != |
| IREE::Util::getRoundedElementByteWidth( |
| storageShapedType.getElementType())) { |
| // TODO(benvanik): more sophisticated logic here. There are a lot of valid |
| // cases that are difficult to account for here statically; for example, |
| // packing 8xi1 into 1xi8 or complex<f32> into 2xf32. We could try to guess |
| // the element count (at least the static part of it) and ensure the scaling |
| // matches but that wouldn't account for user variance. Really with this op |
| // we are letting the _user_ control the bitcasting and type reflection and |
| // purposefully don't want to mess with it (users should be able to put |
| // custom types here, etc). |
| // |
| // NOTE: we round to bytes first as the base type (such as i1) may not be |
| // representable in an external form. |
| // return op->emitOpError() << "encoding and storage types must be " |
| // "bitcastable; adjusted encoding bit width " |
| // "of " |
| // << encodingShapedType.getElementTypeBitWidth() |
| // << " != adjusted storage bit width of " |
| // << storageShapedType.getElementTypeBitWidth(); |
| } |
| |
| if (encodingShapedType.getNumDynamicDims() != |
| storageShapedType.getNumDynamicDims()) { |
| // NOTE: we implicitly require that the dimensions are equivalent but |
| // dont actually care about their order. For example, tensor<?x1xf32> is |
| // compatible with tensor<?xf32>. |
| return op->emitOpError() |
| << "encoding and storage types must have the same " |
| "dynamic dimension values; encoding shape " |
| << encodingShapedType << " incompatible with storage shape " |
| << storageShapedType; |
| } |
| |
| return success(); |
| } |
| |
| LogicalResult TensorImportOp::verify() { |
| TensorImportOp op = *this; |
| auto targetType = llvm::cast<TensorType>(op.getTarget().getType()); |
| if (targetType.getNumDynamicDims() != op.getTargetDims().size()) { |
| return op->emitOpError() << "number of target_dims must match number of " |
| "dynamic dims in target type"; |
| } |
| return verifyTypeStorageCompatibility(op, op.getTargetEncoding(), targetType); |
| } |
| |
| void TensorExportOp::build(OpBuilder &builder, OperationState &result, |
| Type resultType, Value source, |
| TypeAttr sourceEncoding, StringAttr name, |
| Attribute affinity) { |
| auto dynamicDims = |
| IREE::Util::buildDynamicDimsForValue(result.location, source, builder); |
| build(builder, result, resultType, source, sourceEncoding, dynamicDims, name, |
| affinity); |
| } |
| |
| LogicalResult TensorExportOp::verify() { |
| TensorExportOp op = *this; |
| auto sourceType = llvm::cast<TensorType>(op.getSource().getType()); |
| if (sourceType.getNumDynamicDims() != op.getSourceDims().size()) { |
| return op->emitOpError() << "number of source_dims must match number of " |
| "dynamic dims in source type"; |
| } |
| return verifyTypeStorageCompatibility(op, op.getSourceEncoding(), |
| op.getSource().getType()); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.tensor.alias |
| //===----------------------------------------------------------------------===// |
| |
| Value TensorAliasOp::getTiedResult(unsigned resultIndex) { |
| return IREE::Util::TiedOpInterface::findTiedBaseValue(getSource()); |
| } |
| |
| ::std::optional<unsigned> |
| TensorAliasOp::getTiedResultOperandIndex(unsigned resultIndex) { |
| return {0}; // source |
| } |
| |
| SmallVector<int64_t> TensorAliasOp::getTiedResultOperandIndices() { |
| return {0}; // source |
| } |
| |
| LogicalResult TensorAliasOp::verify() { |
| TensorAliasOp op = *this; |
| auto type = llvm::cast<TensorType>(op.getSource().getType()); |
| if (type.getNumDynamicDims() != op.getSourceDims().size()) { |
| return op->emitOpError() |
| << "number of dynamic dims must match the operand type"; |
| } |
| return success(); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.tensor.barrier |
| //===----------------------------------------------------------------------===// |
| |
| void TensorBarrierOp::build(OpBuilder &builder, OperationState &result, |
| ValueRange sources, Value signalFence) { |
| auto resultTypes = llvm::map_to_vector( |
| sources, [](Value source) { return source.getType(); }); |
| build(builder, result, resultTypes, sources, signalFence); |
| } |
| |
| Value TensorBarrierOp::getTiedResult(unsigned resultIndex) { |
| return IREE::Util::TiedOpInterface::findTiedBaseValue( |
| getSources()[resultIndex]); |
| } |
| |
| ::std::optional<unsigned> |
| TensorBarrierOp::getTiedResultOperandIndex(unsigned resultIndex) { |
| return {resultIndex}; // sources[i] |
| } |
| |
| SmallVector<int64_t> TensorBarrierOp::getTiedResultOperandIndices() { |
| size_t numSources = getSources().size(); |
| return llvm::to_vector(llvm::seq<int64_t>(0, numSources)); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.dispatch.extern |
| //===----------------------------------------------------------------------===// |
| |
| void DispatchExternOp::build(OpBuilder &builder, OperationState &state, |
| ValueRange workload, TypeRange resultTypes, |
| ValueRange resultDims, ValueRange arguments, |
| ValueRange argumentDims, |
| ArrayRef<int64_t> tiedOperands, |
| IREE::HAL::ExecutableObjectsAttr targetObjects, |
| ArrayRef<NamedAttribute> attributes) { |
| state.addTypes(resultTypes); |
| state.addOperands(workload); |
| state.addOperands(arguments); |
| state.addOperands(argumentDims); |
| state.addOperands(resultDims); |
| state.addAttributes(attributes); |
| state.attributes.erase(IREE::Util::TiedOpInterface::getStorageAttrName()); |
| state.addAttribute(IREE::Util::TiedOpInterface::getStorageAttrName(), |
| builder.getIndexArrayAttr(tiedOperands)); |
| state.addAttribute("targets", targetObjects.getTargets()); |
| state.addAttribute("target_objects", targetObjects.getTargetObjects()); |
| state.attributes.erase(getOperandSegmentSizeAttr()); |
| state.addAttribute(getOperandSegmentSizeAttr(), |
| builder.getDenseI32ArrayAttr({ |
| static_cast<int32_t>(workload.size()), |
| static_cast<int32_t>(arguments.size()), |
| static_cast<int32_t>(argumentDims.size()), |
| static_cast<int32_t>(resultDims.size()), |
| })); |
| |
| // NOTE: workgroup count region is empty; callers are expected to populate it. |
| state.addRegion(); |
| |
| // Add one empty region per target. |
| for (size_t i = 0; i < targetObjects.getTargets().size(); ++i) |
| state.addRegion(); |
| } |
| |
| // Verifies that |dynamicDims| contains the appropriate number of dims for all |
| // of the dynamic dimensions in |values|. |
| static LogicalResult verifyOpDynamicDims(Operation *op, ValueRange values, |
| ValueRange dynamicDims) { |
| unsigned requiredCount = 0; |
| for (auto value : values) { |
| if (auto shapedType = llvm::dyn_cast<ShapedType>(value.getType())) { |
| requiredCount += shapedType.getNumDynamicDims(); |
| } |
| } |
| if (dynamicDims.size() != requiredCount) { |
| return op->emitOpError() |
| << "value set has " << requiredCount |
| << " dynamic dimensions but only " << dynamicDims.size() |
| << " dimension values are attached"; |
| } |
| return success(); |
| } |
| |
| static LogicalResult |
| verifyWorkgroupCountRegion(Operation *op, ValueRange workload, Region ®ion) { |
| // Verify the workload operands match the expected capture args. |
| auto regionArguments = |
| llvm::make_filter_range(region.getArgumentTypes(), [](Type type) { |
| return !isa<IREE::HAL::DeviceType>(type); |
| }); |
| if (workload.size() != llvm::range_size(regionArguments)) { |
| return op->emitOpError() |
| << "workload operands and workgroup count args mismatch (" |
| << workload.size() << " vs " << llvm::range_size(regionArguments) |
| << ")"; |
| } |
| for (auto [index, values] : |
| llvm::enumerate(llvm::zip_equal(workload, regionArguments))) { |
| auto [workloadValue, capturedType] = values; |
| if (workloadValue.getType() != capturedType) { |
| return op->emitOpError() |
| << "workload value " << index << " type mismatch; operand is " |
| << workloadValue.getType() << " but region captures " |
| << capturedType; |
| } |
| } |
| return success(); |
| } |
| |
| LogicalResult DispatchExternOp::verify() { |
| Operation *op = getOperation(); |
| |
| if (failed(verifyOpDynamicDims(getOperation(), getArguments(), |
| getArgumentDims())) || |
| failed( |
| verifyOpDynamicDims(getOperation(), getResults(), getResultDims()))) { |
| return failure(); |
| } |
| |
| auto verifyIOType = [&](Type type) -> LogicalResult { |
| if (auto shapedType = llvm::dyn_cast<ShapedType>(type)) { |
| if (shapedType.getElementType().isIndex()) { |
| return op->emitOpError() << "I/O type " << type |
| << " is invalid: index types must not cross " |
| "the dispatch boundary"; |
| } |
| } |
| return success(); |
| }; |
| for (auto type : getOperandTypes()) { |
| if (failed(verifyIOType(type))) |
| return failure(); |
| } |
| for (auto type : getResultTypes()) { |
| if (failed(verifyIOType(type))) |
| return failure(); |
| } |
| |
| if (failed( |
| verifyWorkgroupCountRegion(op, getWorkload(), getWorkgroupCount()))) { |
| return failure(); |
| } |
| |
| if (getTargets().size() != getTargetObjects().size()) { |
| return op->emitOpError() << "target and objects arrays must match"; |
| } |
| if (getTargets().size() != getTargetRegions().size()) { |
| return op->emitOpError() |
| << "target and condition regions must match (but they may be empty)"; |
| } |
| for (auto &targetRegion : getTargetRegions()) { |
| if (failed(verifyTargetConditionRegion(op, targetRegion))) { |
| return failure(); |
| } |
| } |
| |
| return success(); |
| } |
| |
| std::pair<unsigned, unsigned> |
| DispatchExternOp::getTiedOperandsIndexAndLength() { |
| return getODSOperandIndexAndLength(1); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.device.memoize |
| //===----------------------------------------------------------------------===// |
| |
| void DeviceMemoizeOp::build(OpBuilder &builder, OperationState &state, |
| TypeRange resultTypes, Value device, |
| Value queueAffinity, |
| ArrayRef<NamedAttribute> attributes) { |
| state.addTypes(resultTypes); |
| state.addOperands(device); |
| state.addOperands(queueAffinity); |
| state.addAttributes(attributes); |
| state.addRegion(); |
| } |
| |
| void DeviceMemoizeOp::getSuccessorRegions( |
| RegionBranchPoint point, SmallVectorImpl<RegionSuccessor> ®ions) { |
| // Unconditional control flow into the region and back to the parent, so |
| // return the correct RegionSuccessor purely based on the index being None or |
| // 0. |
| if (!point.isParent()) { |
| regions.push_back(RegionSuccessor({})); |
| } else { |
| regions.push_back(RegionSuccessor(&getBody(), getBody().getArguments())); |
| } |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.allocator.allocate |
| //===----------------------------------------------------------------------===// |
| |
| void AllocatorAllocateOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| setNameFn(getResult(), "buffer"); |
| } |
| |
| Value AllocatorAllocateOp::getOperandSize(unsigned idx) { return {}; } |
| |
| Value AllocatorAllocateOp::getResultSize(unsigned idx) { |
| return getResultSize(); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.allocator.import |
| //===----------------------------------------------------------------------===// |
| |
| void AllocatorImportOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| setNameFn(getDidImport(), "did_import"); |
| setNameFn(getResult(), "mapped"); |
| } |
| |
| Value AllocatorImportOp::getOperandSize(unsigned idx) { return {}; } |
| |
| Value AllocatorImportOp::getResultSize(unsigned idx) { return getLength(); } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.buffer.subspan |
| //===----------------------------------------------------------------------===// |
| |
| void BufferSubspanOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| setNameFn(getResult(), "buffer"); |
| } |
| |
| Value BufferSubspanOp::getOperandSize(unsigned idx) { return getLength(); } |
| |
| Value BufferSubspanOp::getResultSize(unsigned idx) { return getLength(); } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.buffer.byte_length |
| //===----------------------------------------------------------------------===// |
| |
| void BufferLengthOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| setNameFn(getResult(), "length"); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.element_type |
| //===----------------------------------------------------------------------===// |
| |
| // Keep these in sync with iree/hal/buffer_view.h |
| enum class NumericalType : uint32_t { |
| kUnknown = 0x00, |
| kInteger = 0x10, |
| kIntegerSigned = kInteger | 0x01, |
| kIntegerUnsigned = kInteger | 0x02, |
| kBoolean = kInteger | 0x03, |
| kFloat = 0x20, |
| kFloatIEEE = kFloat | 0x01, |
| kFloatBrain = kFloat | 0x02, |
| kFloatComplex = kFloat | 0x03, |
| kFloat8E5M2 = kFloat | 0x04, |
| kFloat8E4M3 = kFloat | 0x05, |
| kFloat8E5M2FNUZ = kFloat | 0x06, |
| kFloat8E4M3FNUZ = kFloat | 0x07, |
| }; |
| |
| constexpr inline int32_t makeElementTypeValue(NumericalType numericalType, |
| int32_t bitCount) { |
| return (static_cast<uint32_t>(numericalType) << 24) | bitCount; |
| } |
| |
| // static |
| std::optional<int32_t> ElementTypeOp::getTypeValue(Type type) { |
| if (auto intType = llvm::dyn_cast_if_present<IntegerType>(type)) { |
| NumericalType numericalType; |
| if (intType.isInteger(1)) { |
| return makeElementTypeValue(NumericalType::kBoolean, 8); |
| } else if (intType.isSigned()) { |
| numericalType = NumericalType::kIntegerSigned; |
| } else if (intType.isUnsigned()) { |
| numericalType = NumericalType::kIntegerUnsigned; |
| } else { |
| // There's no such thing as a signless integer in machine types but we |
| // need to be able to round-trip the format through the ABI. Exact |
| // numerical type equality comparisons may fail if the frontend assumes |
| // signed/unsigned but the compiler is propagating signless. |
| numericalType = NumericalType::kInteger; |
| } |
| return makeElementTypeValue(numericalType, intType.getWidth()); |
| } else if (auto floatType = llvm::dyn_cast_if_present<FloatType>(type)) { |
| switch (APFloat::SemanticsToEnum(floatType.getFloatSemantics())) { |
| case APFloat::S_Float8E5M2: |
| return makeElementTypeValue(NumericalType::kFloat8E5M2, 8); |
| case APFloat::S_Float8E4M3: |
| return makeElementTypeValue(NumericalType::kFloat8E4M3, 8); |
| case APFloat::S_Float8E5M2FNUZ: |
| return makeElementTypeValue(NumericalType::kFloat8E5M2FNUZ, 8); |
| case APFloat::S_Float8E4M3FNUZ: |
| return makeElementTypeValue(NumericalType::kFloat8E4M3FNUZ, 8); |
| case APFloat::S_IEEEhalf: |
| case APFloat::S_IEEEsingle: |
| case APFloat::S_IEEEdouble: |
| case APFloat::S_IEEEquad: |
| return makeElementTypeValue(NumericalType::kFloatIEEE, |
| floatType.getWidth()); |
| case APFloat::S_BFloat: |
| return makeElementTypeValue(NumericalType::kFloatBrain, |
| floatType.getWidth()); |
| default: |
| return std::nullopt; |
| } |
| } else if (auto complexType = llvm::dyn_cast_if_present<ComplexType>(type)) { |
| return makeElementTypeValue( |
| NumericalType::kFloatComplex, |
| complexType.getElementType().getIntOrFloatBitWidth() * 2); |
| } |
| return std::nullopt; |
| } |
| |
| void ElementTypeOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| // We could make this match the C names. |
| std::string name; |
| llvm::raw_string_ostream os(name); |
| os << "element_type_"; |
| os << getTypeAttr(); |
| setNameFn(getResult(), name); |
| } |
| |
| LogicalResult ElementTypeOp::verify() { |
| ElementTypeOp op = *this; |
| auto value = getTypeValue(getTypeAttr().getValue()); |
| if (!value.has_value()) { |
| return op.emitOpError("unsupported element type"); |
| } |
| return success(); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.encoding_type |
| //===----------------------------------------------------------------------===// |
| |
| // static |
| std::optional<int32_t> EncodingTypeOp::getTypeValue(Attribute attr) { |
| // TODO(#6762): encoding attribute handling/mapping to enums. |
| if (attr) |
| return std::nullopt; |
| // Default to IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR for now. |
| return 1; |
| } |
| |
| void EncodingTypeOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| if (!getEncodingAttr()) |
| setNameFn(getResult(), "dense_row_major"); |
| } |
| |
| LogicalResult EncodingTypeOp::verify() { |
| EncodingTypeOp op = *this; |
| auto value = getTypeValue(getEncodingAttr()); |
| if (!value.has_value()) { |
| return op.emitOpError("unsupported encoding type"); |
| } |
| return success(); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.buffer_view.create |
| //===----------------------------------------------------------------------===// |
| |
| void BufferViewCreateOp::build(OpBuilder &builder, OperationState &state, |
| Value sourceBuffer, Value sourceOffset, |
| Value sourceLength, int32_t elementType, |
| int32_t encodingType, ValueRange shape) { |
| build(builder, state, sourceBuffer, sourceOffset, sourceLength, |
| builder.createOrFold<arith::ConstantIntOp>(state.location, elementType, |
| 32), |
| builder.createOrFold<arith::ConstantIntOp>(state.location, encodingType, |
| 32), |
| shape); |
| } |
| |
| void BufferViewCreateOp::build(OpBuilder &builder, OperationState &state, |
| Value sourceBuffer, Value sourceOffset, |
| Value sourceLength, Value elementType, |
| Value encodingType, ValueRange shape) { |
| state.addOperands( |
| {sourceBuffer, sourceOffset, sourceLength, elementType, encodingType}); |
| state.addOperands(shape); |
| state.addTypes({BufferViewType::get(builder.getContext())}); |
| } |
| |
| void BufferViewCreateOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| setNameFn(getResult(), "view"); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.buffer_view.buffer |
| //===----------------------------------------------------------------------===// |
| |
| void BufferViewBufferOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| setNameFn(getResult(), "buffer"); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.buffer_view.dim |
| //===----------------------------------------------------------------------===// |
| |
| void BufferViewDimOp::inferResultRangesFromOptional( |
| ArrayRef<IntegerValueRange> argRanges, SetIntLatticeFn setResultRange) { |
| const unsigned indexTypeNumBits = 64; |
| setResultRange(getResult(), IntegerValueRange(ConstantIntRanges::fromUnsigned( |
| APInt::getZero(indexTypeNumBits), |
| APInt(indexTypeNumBits, MAX_DIM_VALUE)))); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.buffer_view.dim |
| //===----------------------------------------------------------------------===// |
| |
| void BufferViewRankOp::inferResultRangesFromOptional( |
| ArrayRef<IntegerValueRange> argRanges, SetIntLatticeFn setResultRange) { |
| const unsigned indexTypeNumBits = 64; |
| setResultRange(getResult(), IntegerValueRange(ConstantIntRanges::fromUnsigned( |
| APInt::getZero(indexTypeNumBits), |
| APInt(indexTypeNumBits, MAX_RANK_VALUE)))); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.channel.create |
| //===----------------------------------------------------------------------===// |
| |
| void ChannelCreateOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| setNameFn(getResult(), "channel"); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.channel.split |
| //===----------------------------------------------------------------------===// |
| |
| void ChannelSplitOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| setNameFn(getResult(), "channel"); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.channel.rank_and_count |
| //===----------------------------------------------------------------------===// |
| |
| void ChannelRankAndCountOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| setNameFn(getRank(), "ccl_rank"); |
| setNameFn(getCount(), "ccl_count"); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.command_buffer.create |
| //===----------------------------------------------------------------------===// |
| |
| void CommandBufferCreateOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| setNameFn(getResult(), "cmd"); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.command_buffer.update_buffer |
| //===----------------------------------------------------------------------===// |
| |
| IREE::Util::SubrangeOperand |
| CommandBufferUpdateBufferOp::getSubrangeOperand(unsigned operandIndex) { |
| if (operandIndex == 1) { |
| return IREE::Util::SubrangeOperand{getSourceBuffer(), getSourceSize(), |
| getSourceOffset(), getLength()}; |
| } else { |
| assert(false && "only source is a subrange"); |
| return {}; |
| } |
| } |
| |
| void CommandBufferUpdateBufferOp::setSubrangeOperand( |
| unsigned operandIndex, IREE::Util::SubrangeOperand operand) { |
| if (operandIndex == 1) { |
| getSourceBufferMutable().assign(operand.resource); |
| getSourceSizeMutable().assign(operand.resourceSize); |
| getSourceOffsetMutable().assign(operand.offset); |
| } else { |
| assert(false && "only source is a subrange"); |
| } |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.command_buffer.dispatch + .indirect |
| //===----------------------------------------------------------------------===// |
| |
| void CommandBufferDispatchOp::build(OpBuilder &builder, OperationState &state, |
| Value commandBuffer, Value executable, |
| Value entryPoint, ValueRange workgroups, |
| ValueRange constants, |
| ArrayRef<BindingValue> bindings, |
| IREE::HAL::DispatchFlags flags) { |
| state.addOperands({commandBuffer, executable, entryPoint}); |
| state.addOperands(workgroups); |
| state.addOperands(constants); |
| SmallVector<Value> bindingBuffers; |
| SmallVector<Value> bindingOffsets; |
| SmallVector<Value> bindingLengths; |
| for (auto binding : bindings) { |
| bindingBuffers.push_back(binding.buffer); |
| bindingOffsets.push_back(binding.byteOffset); |
| bindingLengths.push_back(binding.byteLength); |
| } |
| state.addOperands(bindingBuffers); |
| state.addOperands(bindingOffsets); |
| state.addOperands(bindingLengths); |
| state.addAttribute("flags", |
| builder.getAttr<IREE::HAL::DispatchFlagsAttr>(flags)); |
| state.addAttribute(getOperandSegmentSizeAttr(), |
| builder.getDenseI32ArrayAttr({ |
| 1, |
| 1, |
| 1, |
| 1, |
| 1, |
| 1, |
| static_cast<int32_t>(constants.size()), |
| static_cast<int32_t>(bindingBuffers.size()), |
| static_cast<int32_t>(bindingOffsets.size()), |
| static_cast<int32_t>(bindingLengths.size()), |
| })); |
| } |
| |
| void CommandBufferDispatchIndirectOp::build( |
| OpBuilder &builder, OperationState &state, Value commandBuffer, |
| Value executable, Value entryPoint, Value workgroupsBuffer, |
| Value workgroupsOffset, ValueRange constants, |
| ArrayRef<BindingValue> bindings, IREE::HAL::DispatchFlags flags) { |
| state.addOperands({commandBuffer, executable, entryPoint, workgroupsBuffer, |
| workgroupsOffset}); |
| state.addOperands(constants); |
| SmallVector<Value> bindingBuffers; |
| SmallVector<Value> bindingOffsets; |
| SmallVector<Value> bindingLengths; |
| for (auto binding : bindings) { |
| bindingBuffers.push_back(binding.buffer); |
| bindingOffsets.push_back(binding.byteOffset); |
| bindingLengths.push_back(binding.byteLength); |
| } |
| state.addOperands(bindingBuffers); |
| state.addOperands(bindingOffsets); |
| state.addOperands(bindingLengths); |
| state.addAttribute("flags", |
| builder.getAttr<IREE::HAL::DispatchFlagsAttr>(flags)); |
| state.addAttribute(getOperandSegmentSizeAttr(), |
| builder.getDenseI32ArrayAttr({ |
| 1, |
| 1, |
| 1, |
| 1, |
| 1, |
| static_cast<int32_t>(constants.size()), |
| static_cast<int32_t>(bindingBuffers.size()), |
| static_cast<int32_t>(bindingOffsets.size()), |
| static_cast<int32_t>(bindingLengths.size()), |
| })); |
| } |
| |
| static LogicalResult verifyDispatchBindings(Operation *op, |
| ValueRange bindingBuffers, |
| ValueRange bindingOffsets, |
| ValueRange bindingLengths) { |
| if (bindingBuffers.size() != bindingOffsets.size() || |
| bindingBuffers.size() != bindingLengths.size()) { |
| return op->emitOpError() << "requires that binding fields all have the " |
| "same number of elements"; |
| } |
| return success(); |
| } |
| |
| LogicalResult CommandBufferDispatchOp::verify() { |
| CommandBufferDispatchOp op = *this; |
| return verifyDispatchBindings(op, op.getBindingBuffers(), |
| op.getBindingOffsets(), op.getBindingLengths()); |
| } |
| |
| LogicalResult CommandBufferDispatchIndirectOp::verify() { |
| CommandBufferDispatchIndirectOp op = *this; |
| return verifyDispatchBindings(op, op.getBindingBuffers(), |
| op.getBindingOffsets(), op.getBindingLengths()); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.device.resolve |
| //===----------------------------------------------------------------------===// |
| |
| void DeviceResolveOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| for (auto result : getResults()) { |
| if (isa<IREE::HAL::DeviceType>(result.getType())) { |
| setNameFn(result, "device"); |
| } else if (isa<IREE::HAL::AllocatorType>(result.getType())) { |
| setNameFn(result, "allocator"); |
| } else if (isa<IntegerType>(result.getType())) { |
| setNameFn(result, "queue_affinity"); |
| } |
| } |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.device.allocator |
| //===----------------------------------------------------------------------===// |
| |
| void DeviceAllocatorOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| setNameFn(getResult(), "allocator"); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.device.query |
| //===----------------------------------------------------------------------===// |
| |
| LogicalResult DeviceQueryOp::verify() { |
| DeviceQueryOp op = *this; |
| if (op.getDefaultValue().has_value()) { |
| if (op.getDefaultValue()->getType() != op.getValue().getType()) { |
| return op.emitOpError() |
| << "type mismatch between result and default value"; |
| } |
| } |
| return success(); |
| } |
| |
| // static |
| Value DeviceQueryOp::createI1(Location loc, Value device, StringRef category, |
| StringRef key, OpBuilder &builder) { |
| auto i1Type = builder.getI1Type(); |
| return builder |
| .create<IREE::HAL::DeviceQueryOp>( |
| loc, i1Type, i1Type, device, builder.getStringAttr(category), |
| builder.getStringAttr(key), builder.getIntegerAttr(i1Type, 0)) |
| .getValue(); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.device.queue.* |
| //===----------------------------------------------------------------------===// |
| |
| void DeviceQueueAllocaOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| setNameFn(getResult(), "transient_buffer"); |
| } |
| |
| Value DeviceQueueAllocaOp::getOperandSize(unsigned idx) { return {}; } |
| |
| Value DeviceQueueAllocaOp::getResultSize(unsigned idx) { |
| return getResultSize(); |
| } |
| |
| static LogicalResult verifyDeviceQueueFences(Operation *queueOp, |
| Value waitFence, |
| Value signalFence) { |
| if (waitFence == signalFence && |
| !isa<IREE::Util::NullOp>(waitFence.getDefiningOp())) { |
| return queueOp->emitOpError() << "device queue operations cannot wait and " |
| "signal on the same fence."; |
| } |
| return success(); |
| } |
| |
| LogicalResult DeviceQueueAllocaOp::verify() { |
| return verifyDeviceQueueFences(*this, getWaitFence(), getSignalFence()); |
| } |
| |
| LogicalResult DeviceQueueDeallocaOp::verify() { |
| return verifyDeviceQueueFences(*this, getWaitFence(), getSignalFence()); |
| } |
| |
| LogicalResult DeviceQueueFillOp::verify() { |
| return verifyDeviceQueueFences(*this, getWaitFence(), getSignalFence()); |
| } |
| |
| LogicalResult DeviceQueueUpdateOp::verify() { |
| return verifyDeviceQueueFences(*this, getWaitFence(), getSignalFence()); |
| } |
| |
| LogicalResult DeviceQueueCopyOp::verify() { |
| return verifyDeviceQueueFences(*this, getWaitFence(), getSignalFence()); |
| } |
| |
| LogicalResult DeviceQueueReadOp::verify() { |
| return verifyDeviceQueueFences(*this, getWaitFence(), getSignalFence()); |
| } |
| |
| LogicalResult DeviceQueueWriteOp::verify() { |
| return verifyDeviceQueueFences(*this, getWaitFence(), getSignalFence()); |
| } |
| |
| LogicalResult DeviceQueueExecuteOp::verify() { |
| return verifyDeviceQueueFences(*this, getWaitFence(), getSignalFence()); |
| } |
| |
| void DeviceQueueExecuteIndirectOp::build(OpBuilder &builder, |
| OperationState &state, Value device, |
| Value queueAffinity, Value waitFence, |
| Value signalFence, Value commandBuffer, |
| ArrayRef<BindingValue> bindings) { |
| state.addOperands( |
| {device, queueAffinity, waitFence, signalFence, commandBuffer}); |
| SmallVector<Value> bindingBuffers; |
| SmallVector<Value> bindingOffsets; |
| SmallVector<Value> bindingLengths; |
| for (auto binding : bindings) { |
| bindingBuffers.push_back(binding.buffer); |
| bindingOffsets.push_back(binding.byteOffset); |
| bindingLengths.push_back(binding.byteLength); |
| } |
| state.addOperands(bindingBuffers); |
| state.addOperands(bindingOffsets); |
| state.addOperands(bindingLengths); |
| } |
| |
| LogicalResult DeviceQueueExecuteIndirectOp::verify() { |
| return verifyDeviceQueueFences(*this, getWaitFence(), getSignalFence()); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.devices.* |
| //===----------------------------------------------------------------------===// |
| |
| void DevicesCountOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| setNameFn(getResult(), "device_count"); |
| } |
| |
| void DevicesGetOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| APInt index; |
| if (matchPattern(getIndex(), m_ConstantInt(&index))) { |
| llvm::SmallString<16> str("device_"); |
| index.toStringUnsigned(str); |
| setNameFn(getResult(), str); |
| } else { |
| setNameFn(getResult(), "device_n"); |
| } |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.executable.source |
| //===----------------------------------------------------------------------===// |
| |
| LogicalResult ExecutableSourceOp::verify() { |
| ExecutableSourceOp op = *this; |
| |
| auto conditionOps = getOps<IREE::HAL::ExecutableConditionOp>(); |
| if (llvm::range_size(conditionOps) > 1) |
| return op.emitOpError() |
| << "only one condition op is allowed in an executable"; |
| |
| return success(); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.executable |
| //===----------------------------------------------------------------------===// |
| |
| void ExecutableOp::build(OpBuilder &builder, OperationState &state, |
| StringRef name) { |
| ensureTerminator(*state.addRegion(), builder, state.location); |
| state.addAttribute(mlir::SymbolTable::getSymbolAttrName(), |
| builder.getStringAttr(name)); |
| } |
| |
| LogicalResult ExecutableOp::verify() { |
| // TODO(benvanik): check export name conflicts. |
| return success(); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.executable.export |
| //===----------------------------------------------------------------------===// |
| |
| ParseResult ExecutableExportOp::parse(OpAsmParser &parser, |
| OperationState &result) { |
| StringAttr visibilityAttr; |
| if (failed(parseSymbolVisibility(parser, visibilityAttr))) { |
| return failure(); |
| } |
| |
| StringAttr nameAttr; |
| IREE::HAL::PipelineLayoutAttr layoutAttr; |
| if (failed(parser.parseSymbolName(nameAttr, |
| mlir::SymbolTable::getSymbolAttrName(), |
| result.attributes))) { |
| return failure(); |
| } |
| if (succeeded(parser.parseOptionalKeyword("ordinal"))) { |
| IntegerAttr ordinalAttr; |
| if (failed(parser.parseLParen()) || |
| failed(parser.parseAttribute(ordinalAttr, |
| parser.getBuilder().getIndexType())) || |
| failed(parser.parseRParen())) { |
| return failure(); |
| } |
| result.addAttribute("ordinal", ordinalAttr); |
| } |
| if (failed(parser.parseKeyword("layout")) || failed(parser.parseLParen()) || |
| failed(parser.parseAttribute(layoutAttr)) || |
| failed(parser.parseRParen()) || |
| failed(parser.parseOptionalAttrDictWithKeyword(result.attributes))) { |
| return failure(); |
| } |
| result.addAttribute("layout", layoutAttr); |
| |
| std::unique_ptr<Region> region; |
| SmallVector<OpAsmParser::Argument> regionOperands; |
| // A missing optional region is materialized as an empty region. |
| (void)parser.parseOptionalRegion(region, regionOperands); |
| result.addRegion(std::move(region)); |
| |
| return success(); |
| } |
| |
| void ExecutableExportOp::print(OpAsmPrinter &p) { |
| Operation *op = getOperation(); |
| p << ' '; |
| printSymbolVisibility(p, op, op->getAttrOfType<StringAttr>("sym_visibility")); |
| p << ' '; |
| p.printSymbolName(getSymName()); |
| if (getOrdinalAttr()) { |
| p << " ordinal("; |
| p.printAttributeWithoutType(getOrdinalAttr()); |
| p << ")"; |
| } |
| p << " layout("; |
| p.printAttribute(getLayout()); |
| p << ")"; |
| p.printOptionalAttrDictWithKeyword( |
| op->getAttrs(), |
| /*elidedAttrs=*/{"sym_name", "layout", "ordinal"}); |
| if (getWorkgroupCount().empty()) |
| return; |
| p << " "; |
| p.printRegion(getWorkgroupCount()); |
| } |
| |
| LogicalResult ExecutableExportOp::verify() { |
| ExecutableExportOp op = *this; |
| Block *body = getWorkgroupCountBody(); |
| // When there is no body, nothing to verify. |
| if (!body) |
| return success(); |
| |
| if (!llvm::hasSingleElement(getWorkgroupCount())) { |
| return op.emitOpError() << "expected a single region block"; |
| } |
| bool validArguments = true; |
| if (body->getNumArguments() == 0) { |
| // Need at least a !hal.device. |
| validArguments = false; |
| } else if (!llvm::isa<IREE::HAL::DeviceType>( |
| body->getArgument(0).getType())) { |
| // !hal.device must come first. |
| validArguments = false; |
| } else { |
| // All remaining arguments need to be of type index (today). |
| for (BlockArgument &blockArg : body->getArguments().drop_front(1)) { |
| if (!llvm::isa<IndexType>(blockArg.getType())) { |
| validArguments = false; |
| break; |
| } |
| } |
| } |
| if (!validArguments) { |
| return op.emitOpError( |
| "expected workgroup_count to take (%device: !hal.device, " |
| "%workload_0: index, %workload_1: index, ..."); |
| } |
| // Check that the last statement in the block is `hal.return` operation. |
| // TODO(ravishankarm): The SingleBlockImplicitTerminator<"HAL::ReturnOp"> |
| // should generate this check, but it doesnt. |
| auto returnOp = dyn_cast<ReturnOp>(body->getTerminator()); |
| if (!returnOp || returnOp.getOperands().size() != getNumWorkgroupDims()) { |
| return op.emitOpError("expected operation to yield ") |
| << getNumWorkgroupDims() << " values"; |
| } |
| return success(); |
| } |
| |
| // Calculates the workgroup count (x, y, z) given the total N-dimensional |
| // |workload| and specific |workgroupSize|. |
| static std::array<Value, 3> |
| calculateWorkloadWorkgroupCount(Location loc, ValueRange workload, |
| const std::array<Value, 3> &workgroupSize, |
| OpBuilder &builder) { |
| std::array<Value, 3> result; |
| |
| auto constantOne = builder.createOrFold<arith::ConstantIndexOp>(loc, 1); |
| if (workload.size() <= 3) { |
| // 1-D to 3-D are easy (pad 2 to 0 dimensions) and divide by workgroup |
| // size. |
| for (int i = 0; i < 3; ++i) { |
| // Round up: (workload[i] + workgroup_size - 1) / workgroup_size; |
| Value workloadI = i < workload.size() ? workload[i] : constantOne; |
| workloadI = builder.createOrFold<arith::SubIOp>( |
| loc, |
| builder.createOrFold<arith::AddIOp>(loc, workloadI, workgroupSize[i]), |
| constantOne); |
| result[i] = builder.createOrFold<arith::DivUIOp>(loc, workloadI, |
| workgroupSize[i]); |
| } |
| } else { |
| // TODO(#4140): remapping of N-D to 3-D: this is not how you do this! |
| Value flatWorkload = constantOne; |
| for (auto workloadI : workload) { |
| flatWorkload = |
| builder.createOrFold<arith::MulIOp>(loc, flatWorkload, workloadI); |
| } |
| for (int i = 0; i < 3; ++i) { |
| // Round up: (workload[i] + workgroup_size - 1) / workgroup_size; |
| auto rounded = builder.createOrFold<arith::SubIOp>( |
| loc, |
| builder.createOrFold<arith::AddIOp>(loc, flatWorkload, |
| workgroupSize[i]), |
| constantOne); |
| auto workgroupCountI = |
| builder.createOrFold<arith::DivUIOp>(loc, rounded, workgroupSize[i]); |
| result[i] = workgroupCountI; |
| |
| // Multiply back out and subtract from invocations. |
| flatWorkload = builder.createOrFold<arith::SubIOp>( |
| loc, flatWorkload, |
| builder.createOrFold<arith::MulIOp>(loc, workgroupCountI, rounded)); |
| } |
| } |
| |
| return result; |
| } |
| |
| static std::array<Value, 3> |
| calculateWorkgroupCountFromRegion(Location loc, Block *body, Value device, |
| ValueRange workload, OpBuilder &builder) { |
| // TODO(benvanik): replace with region inlining util. |
| IRMapping bvm; |
| bvm.map(body->getArgument(0), device); |
| // For now use the number of args to minimum of number of args used by |
| // the body, and number of workload entries. When there is a more explicit |
| // propagation of number of workload entries to the `hal.executable.variant` |
| // this will be the same by construction. |
| unsigned numArgs = |
| std::min<unsigned>(body->getNumArguments() - 1, workload.size()); |
| for (unsigned argNum : llvm::seq<unsigned>(0, numArgs)) { |
| bvm.map(body->getArgument(/*device*/ 1 + argNum), workload[argNum]); |
| } |
| for (Operation &op : body->without_terminator()) { |
| builder.clone(op, bvm); |
| } |
| auto returnOp = cast<IREE::HAL::ReturnOp>(body->getTerminator()); |
| assert(returnOp.getNumOperands() == 3 && "must return xyz"); |
| return { |
| bvm.lookup(returnOp.getOperands()[0]), |
| bvm.lookup(returnOp.getOperands()[1]), |
| bvm.lookup(returnOp.getOperands()[2]), |
| }; |
| } |
| |
| // Calculates the workgroup count (x, y, z) for dispatching to the entry point. |
| // The provided N-dimensional |workload| is the total number of invocations |
| // required as calculated by the generic workload logic (basically, number of |
| // output elements in tensors). |
| std::array<Value, 3> ExecutableExportOp::calculateWorkgroupCount( |
| Location loc, Value device, ValueRange workload, OpBuilder &builder) { |
| Block *body = getWorkgroupCountBody(); |
| if (body) { |
| return calculateWorkgroupCountFromRegion(loc, body, device, workload, |
| builder); |
| } |
| auto workgroupSize = calculateWorkgroupSize(loc, device, workload, builder); |
| return calculateWorkloadWorkgroupCount(loc, workload, workgroupSize, builder); |
| } |
| |
| // Calculates the workgroup size (x, y, z). These are the dimension numbers |
| // for a single workgroup. |
| std::array<Value, 3> ExecutableExportOp::calculateWorkgroupSize( |
| Location loc, Value device, ValueRange workload, OpBuilder &builder) { |
| // When no workgroup size is specified we just assume [1,1,1]. |
| // This yields a workgroup count that models the extents of the workload. |
| return { |
| builder.createOrFold<arith::ConstantIndexOp>(loc, 1), |
| builder.createOrFold<arith::ConstantIndexOp>(loc, 1), |
| builder.createOrFold<arith::ConstantIndexOp>(loc, 1), |
| }; |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.executable.variant |
| //===----------------------------------------------------------------------===// |
| |
| void ExecutableVariantOp::build(OpBuilder &builder, OperationState &state, |
| StringRef symName, |
| IREE::HAL::ExecutableTargetAttr target) { |
| ensureTerminator(*state.addRegion(), builder, state.location); |
| state.addAttribute(mlir::SymbolTable::getSymbolAttrName(), |
| builder.getStringAttr(symName)); |
| state.addAttribute("target", target); |
| } |
| |
| LogicalResult ExecutableVariantOp::verify() { |
| ExecutableVariantOp op = *this; |
| |
| auto conditionOps = getOps<IREE::HAL::ExecutableConditionOp>(); |
| if (llvm::range_size(conditionOps) > 1) |
| return op.emitOpError() << "only one condition op is allowed in a variant"; |
| |
| return success(); |
| } |
| |
| DenseMap<Attribute, int> ExecutableVariantOp::gatherConstantOrdinals() { |
| DenseMap<Attribute, int> map; |
| for (auto blockOp : getConstantBlockOps()) { |
| int baseCount = map.size(); |
| for (auto [i, keyAttr] : llvm::enumerate(blockOp.getKeys())) { |
| map.try_emplace(keyAttr, baseCount + i); |
| } |
| } |
| return map; |
| } |
| |
| Value ExecutableVariantOp::createConditionOp(OpBuilder &builder) { |
| assert(!getConditionOp() && "condition op already exists"); |
| |
| builder.setInsertionPointToStart(&getRegion().front()); |
| auto conditionOp = builder.create<IREE::HAL::ExecutableConditionOp>(getLoc()); |
| Block *entryPoint = conditionOp.addEntryBlock(); |
| Value device = entryPoint->getArgument(0); |
| |
| builder.setInsertionPointToStart(entryPoint); |
| return device; |
| } |
| |
| Value ExecutableVariantOp::buildCondition(Value device, OpBuilder &builder) { |
| // Base case dependent on target information. |
| Value selected = IREE::HAL::DeviceQueryOp::createI1( |
| getLoc(), device, "hal.executable.format", |
| getTarget().getFormat().getValue(), builder); |
| |
| // Factor in variant condition region, if any. |
| auto conditionOp = getConditionOp(); |
| if (conditionOp) { |
| auto regionOp = builder.create<scf::ExecuteRegionOp>(conditionOp.getLoc(), |
| builder.getI1Type()); |
| |
| IRMapping mapper; |
| mapper.map(conditionOp.getRegion().getArgument(0), device); |
| conditionOp.getRegion().cloneInto(®ionOp.getRegion(), mapper); |
| |
| for (auto returnOp : |
| llvm::make_early_inc_range(regionOp.getOps<IREE::HAL::ReturnOp>())) { |
| OpBuilder(returnOp).create<scf::YieldOp>(returnOp.getLoc(), |
| returnOp.getOperands()); |
| returnOp.erase(); |
| } |
| |
| selected = builder.create<arith::AndIOp>(getLoc(), selected, |
| regionOp.getResult(0)); |
| } |
| |
| return selected; |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.executable.condition |
| //===----------------------------------------------------------------------===// |
| |
| LogicalResult ExecutableConditionOp::verify() { |
| ExecutableConditionOp op = *this; |
| return verifyTargetConditionRegion(op, op.getBody()); |
| } |
| |
| void ExecutableConditionOp::build(OpBuilder &builder, OperationState &result, |
| ArrayRef<NamedAttribute> attrs) { |
| result.addAttribute( |
| "function_type", |
| TypeAttr::get(getTargetConditionRegionType(builder.getContext()))); |
| result.addRegion(); |
| result.attributes.append(attrs.begin(), attrs.end()); |
| } |
| |
| ParseResult ExecutableConditionOp::parse(OpAsmParser &parser, |
| OperationState &result) { |
| if (parseTargetConditionRegion(parser, *result.addRegion())) |
| return failure(); |
| result.addAttribute( |
| "function_type", |
| TypeAttr::get(getTargetConditionRegionType(parser.getContext()))); |
| if (parser.parseOptionalAttrDictWithKeyword(result.attributes)) |
| return failure(); |
| return success(); |
| } |
| |
| void ExecutableConditionOp::print(OpAsmPrinter &p) { |
| Operation *op = getOperation(); |
| printTargetConditionRegion(p, op, getBody()); |
| p.printOptionalAttrDictWithKeyword(op->getAttrs(), |
| /*elidedAttrs=*/{"function_type"}); |
| } |
| |
| Block *ExecutableConditionOp::addEntryBlock() { |
| assert(empty() && "function already has an entry block"); |
| auto *entry = new Block(); |
| auto argTypes = getArgumentTypes(); |
| SmallVector<Location> argLocs(argTypes.size(), getLoc()); |
| entry->addArguments(argTypes, argLocs); |
| push_back(entry); |
| return entry; |
| } |
| |
| Block *ExecutableConditionOp::addBlock() { |
| assert(!empty() && "function should at least have an entry block"); |
| push_back(new Block()); |
| return &back(); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.executable.constant.block |
| //===----------------------------------------------------------------------===// |
| |
| ParseResult ExecutableConstantBlockOp::parse(OpAsmParser &parser, |
| OperationState &result) { |
| auto &builder = parser.getBuilder(); |
| |
| // Parse the function signature. |
| SmallVector<OpAsmParser::Argument> entryArgs; |
| bool isVariadic = false; |
| SmallVector<DictionaryAttr> resultAttrs; |
| SmallVector<Type> resultTypes; |
| if (mlir::function_interface_impl::parseFunctionSignature( |
| parser, /*allowVariadic=*/false, entryArgs, isVariadic, resultTypes, |
| resultAttrs)) { |
| return failure(); |
| } |
| SmallVector<Type> argTypes; |
| for (auto &arg : entryArgs) |
| argTypes.push_back(arg.type); |
| auto fnType = builder.getFunctionType(argTypes, resultTypes); |
| result.addAttribute(getFunctionTypeAttrName(result.name), |
| TypeAttr::get(fnType)); |
| |
| // Parse the keys used for each yielded constant value. |
| // There must be one key per result. Note that we support omitted parens when |
| // only one result is present. |
| SmallVector<Attribute> keyAttrs; |
| if (failed(parser.parseKeyword("as"))) |
| return failure(); |
| if (resultTypes.size() == 1) { |
| std::string key; |
| if (failed(parser.parseString(&key))) |
| return failure(); |
| keyAttrs.push_back(builder.getStringAttr(key)); |
| } else { |
| if (failed(parser.parseCommaSeparatedList( |
| AsmParser::Delimiter::OptionalParen, |
| [&]() { |
| std::string key; |
| if (failed(parser.parseString(&key))) |
| return failure(); |
| keyAttrs.push_back(builder.getStringAttr(key)); |
| return success(); |
| }, |
| "containing a 1:1 list of keys per yielded value"))) { |
| return failure(); |
| } |
| } |
| result.addAttribute("keys", builder.getArrayAttr(keyAttrs)); |
| |
| // If function attributes are present, parse them. |
| NamedAttrList parsedAttributes; |
| if (parser.parseOptionalAttrDictWithKeyword(parsedAttributes)) { |
| return failure(); |
| } |
| result.attributes.append(parsedAttributes); |
| |
| // Add the attributes to the function arguments. |
| assert(resultAttrs.size() == resultTypes.size()); |
| mlir::function_interface_impl::addArgAndResultAttrs( |
| builder, result, entryArgs, resultAttrs, getArgAttrsAttrName(result.name), |
| getResAttrsAttrName(result.name)); |
| |
| // Parse the optional function body. The printer will not print the body if |
| // its empty, so disallow parsing of empty body in the parser. |
| auto *body = result.addRegion(); |
| SMLoc loc = parser.getCurrentLocation(); |
| if (failed(parser.parseRegion(*body, entryArgs, |
| /*enableNameShadowing=*/false))) { |
| return failure(); |
| } |
| // Function body was parsed, make sure its not empty. |
| if (body->empty()) { |
| return parser.emitError(loc, "expected non-empty function body"); |
| } |
| |
| return success(); |
| } |
| |
| void ExecutableConstantBlockOp::print(OpAsmPrinter &p) { |
| Operation *op = getOperation(); |
| ArrayRef<Type> argTypes = getArgumentTypes(); |
| ArrayRef<Type> resultTypes = getResultTypes(); |
| mlir::function_interface_impl::printFunctionSignature( |
| p, cast<mlir::FunctionOpInterface>(op), argTypes, /*isVariadic=*/false, |
| resultTypes); |
| p << " as "; |
| if (resultTypes.size() != 1) |
| p << '('; |
| llvm::interleaveComma(getKeys().getValue(), p, |
| [&](Attribute attr) { p << attr; }); |
| if (resultTypes.size() != 1) |
| p << ')'; |
| mlir::function_interface_impl::printFunctionAttributes( |
| p, op, {getFunctionTypeAttrName(), getKeysAttrName()}); |
| p << " "; |
| p.printRegion(getBody(), /*printEntryBlockArgs=*/false, |
| /*printBlockTerminators=*/true); |
| } |
| |
| LogicalResult ExecutableConstantBlockOp::verify() { |
| ExecutableConstantBlockOp op = *this; |
| |
| // Verify the function takes either nothing or a device. |
| auto argTypes = op.getArgumentTypes(); |
| if (!argTypes.empty() && |
| (argTypes.size() > 1 || !llvm::isa<IREE::HAL::DeviceType>(argTypes[0]))) { |
| return op->emitOpError() |
| << "initializer must take a !hal.device or nothing"; |
| } |
| |
| // Verify the return types are all i32 (today). |
| for (auto resultType : llvm::enumerate(op.getResultTypes())) { |
| if (!resultType.value().isInteger(32)) { |
| return op->emitOpError() |
| << "initializer must return only i32 values (result " |
| << resultType.index() << " is " << resultType.value() << ")"; |
| } |
| } |
| |
| // Verify there's a key for every result. |
| if (op.getNumResults() != op.getKeys().size()) { |
| return op->emitOpError() << "must have one key for every result"; |
| } |
| |
| return success(); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.executable.binary |
| //===----------------------------------------------------------------------===// |
| |
| void ExecutableBinaryOp::build(OpBuilder &builder, OperationState &state, |
| StringRef symName, StringRef format, |
| std::vector<uint8_t> data) { |
| state.addAttribute(mlir::SymbolTable::getSymbolAttrName(), |
| builder.getStringAttr(symName)); |
| state.addAttribute("format", builder.getStringAttr(format)); |
| state.addAttribute("data", |
| DenseIntElementsAttr::get( |
| VectorType::get({static_cast<int64_t>(data.size())}, |
| builder.getIntegerType(8)), |
| data)); |
| } |
| |
| void ExecutableBinaryOp::build(OpBuilder &builder, OperationState &state, |
| StringRef symName, StringAttr format, |
| DenseIntElementsAttr data) { |
| state.addAttribute(mlir::SymbolTable::getSymbolAttrName(), |
| builder.getStringAttr(symName)); |
| state.addAttribute("format", format); |
| state.addAttribute("data", data); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.executable.create |
| //===----------------------------------------------------------------------===// |
| |
| void ExecutableCreateOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| // TODO(benvanik): name after sanitized symbol. |
| setNameFn(getResult(), StringRef("executable")); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.executable.lookup |
| //===----------------------------------------------------------------------===// |
| |
| void ExecutableLookupOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| // TODO(benvanik): name after sanitized symbol. |
| setNameFn(getResult(), "exe"); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.executable.export.ordinal |
| //===----------------------------------------------------------------------===// |
| |
| void ExecutableExportOrdinalOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| // TODO(benvanik): name after sanitized symbol. |
| setNameFn(getResult(), "ordinal"); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.interface.constant.load |
| //===----------------------------------------------------------------------===// |
| |
| LogicalResult InterfaceConstantLoadOp::verify() { |
| InterfaceConstantLoadOp op = *this; |
| auto layoutAttr = op.getLayout(); |
| if (op.getOrdinal().getZExtValue() >= layoutAttr.getConstants()) { |
| return op.emitOpError("push constant ordinal out of bounds"); |
| } |
| return success(); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.interface.binding.subspan |
| //===----------------------------------------------------------------------===// |
| |
| void InterfaceBindingSubspanOp::build(OpBuilder &builder, |
| OperationState &result, Type resultType, |
| IREE::HAL::PipelineLayoutAttr layout, |
| APInt binding, Value byte_offset, |
| ValueRange dynamic_dims, |
| IntegerAttr alignment, |
| std::optional<DescriptorFlags> flags) { |
| IREE::HAL::DescriptorFlagsAttr descriptorAttr; |
| if (flags.has_value()) { |
| descriptorAttr = IREE::HAL::DescriptorFlagsAttr::get(builder.getContext(), |
| flags.value()); |
| } |
| build(builder, result, resultType, layout, binding, byte_offset, dynamic_dims, |
| alignment, descriptorAttr); |
| } |
| |
| LogicalResult InterfaceBindingSubspanOp::verify() { |
| InterfaceBindingSubspanOp op = *this; |
| if (ShapedType shapedType = llvm::dyn_cast<ShapedType>(op.getType())) { |
| if (shapedType.getNumDynamicDims() != op.getDynamicDims().size()) { |
| return op.emitOpError("result type ") |
| << op.getType() << " has " << shapedType.getNumDynamicDims() |
| << " dynamic dimensions but " << op.getDynamicDims().size() |
| << " associated dimension SSA values"; |
| } |
| } |
| uint64_t binding = op.getBinding().getZExtValue(); |
| if (binding >= op.getLayout().getBindings().size()) { |
| return op.emitOpError("binding ordinal ") |
| << binding << " out of bounds in layout " << op.getLayout(); |
| } |
| return success(); |
| } |
| |
| IREE::HAL::PipelineBindingAttr |
| InterfaceBindingSubspanOp::getPipelineBindingAttr() { |
| return getLayout().getBinding(getBinding()); |
| } |
| |
| IREE::HAL::DescriptorType InterfaceBindingSubspanOp::getDescriptorType() { |
| auto bindingAttr = getPipelineBindingAttr(); |
| return bindingAttr.getType(); |
| } |
| |
| llvm::MaybeAlign InterfaceBindingSubspanOp::getBaseAlignment() { |
| if (auto baseAlignmentInt = getAlignment()) { |
| return llvm::MaybeAlign(baseAlignmentInt.value().getZExtValue()); |
| } |
| return std::nullopt; |
| } |
| |
| llvm::Align InterfaceBindingSubspanOp::calculateAlignment() { |
| // If we can't calculate an alignment we fall back to the natural alignment of |
| // the element type (for example, a memref<?xi32> is known to be at least |
| // 4-byte aligned). |
| llvm::Align naturalAlignment(1); |
| auto resultType = getType(); |
| if (auto shapedType = llvm::dyn_cast<ShapedType>(resultType)) { |
| naturalAlignment = llvm::Align( |
| IREE::Util::getRoundedElementByteWidth(shapedType.getElementType())); |
| } |
| |
| // If the binding has no assigned alignment we fall back to natural alignment. |
| auto baseAlignment = getBaseAlignment(); |
| if (!baseAlignment) |
| return naturalAlignment; |
| |
| // If there's no offset specified then we can use the binding alignment |
| // directly. |
| if (!getByteOffset()) |
| return baseAlignment.value(); |
| |
| // Try to get the alignment of the byte offset. If it's a constant then we can |
| // find a common alignment between it and the base and otherwise we need to |
| // try to infer the alignment from the IR - otherwise we fall back. |
| auto offsetOrAlignment = lookupOffsetOrAlignment(getByteOffset()); |
| if (!offsetOrAlignment.has_value()) |
| return naturalAlignment; |
| |
| // Compute the common alignment between that of the binding base and that of |
| // the byte offset. |
| return llvm::commonAlignment(baseAlignment.value(), |
| offsetOrAlignment.value()); |
| } |
| |
| LogicalResult InterfaceBindingSubspanOp::reifyResultShapes( |
| OpBuilder &builder, ReifiedRankedShapedTypeDims &reifiedReturnShapes) { |
| auto resultShapedType = dyn_cast<ShapedType>(getResult().getType()); |
| if (!resultShapedType) { |
| return failure(); |
| } |
| SmallVector<OpFoldResult> resultShape = mlir::getMixedValues( |
| resultShapedType.getShape(), getDynamicDims(), builder); |
| reifiedReturnShapes.emplace_back(std::move(resultShape)); |
| return success(); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.interface.workgroup.* |
| //===----------------------------------------------------------------------===// |
| |
| static void getAsmResultNamesForInterfaceWorkgroupOp( |
| StringRef prefix, const APInt &dimension, Value result, |
| function_ref<void(Value, StringRef)> setNameFn) { |
| switch (dimension.getZExtValue()) { |
| case 0: |
| setNameFn(result, (prefix + "x").str()); |
| return; |
| case 1: |
| setNameFn(result, (prefix + "y").str()); |
| return; |
| case 2: |
| setNameFn(result, (prefix + "z").str()); |
| return; |
| } |
| } |
| |
| // Minimum is the smallest possible result we could get. It's 0 for ID-like |
| // operations and 1 for count-like ones. |
| static void setResultRangesForInterfaceWorkgroupOp( |
| Value result, const std::optional<APInt> &upperBound, |
| SetIntRangeFn setResultRanges, int64_t minimum) { |
| unsigned width = ConstantIntRanges::getStorageBitwidth(result.getType()); |
| if (!upperBound.has_value()) { |
| setResultRanges( |
| result, ConstantIntRanges::fromSigned(APInt(width, minimum), |
| APInt::getSignedMaxValue(width))); |
| return; |
| } |
| setResultRanges(result, |
| ConstantIntRanges::fromUnsigned(APInt(width, minimum), |
| *upperBound + minimum - 1)); |
| } |
| |
| void InterfaceWorkgroupIDOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| getAsmResultNamesForInterfaceWorkgroupOp("workgroup_id_", getDimension(), |
| getResult(), setNameFn); |
| } |
| |
| void InterfaceWorkgroupIDOp::inferResultRanges( |
| ArrayRef<ConstantIntRanges> argRanges, SetIntRangeFn setResultRanges) { |
| setResultRangesForInterfaceWorkgroupOp(getResult(), getUpperBound(), |
| setResultRanges, /*minimum=*/0); |
| } |
| |
| void InterfaceWorkgroupCountOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| getAsmResultNamesForInterfaceWorkgroupOp("workgroup_count_", getDimension(), |
| getResult(), setNameFn); |
| } |
| |
| void InterfaceWorkgroupCountOp::inferResultRanges( |
| ArrayRef<ConstantIntRanges> argRanges, SetIntRangeFn setResultRanges) { |
| setResultRangesForInterfaceWorkgroupOp(getResult(), getUpperBound(), |
| setResultRanges, /*minimum=*/1); |
| } |
| |
| void InterfaceWorkgroupSizeOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| getAsmResultNamesForInterfaceWorkgroupOp("workgroup_size_", getDimension(), |
| getResult(), setNameFn); |
| } |
| |
| void InterfaceWorkgroupSizeOp::inferResultRanges( |
| ArrayRef<ConstantIntRanges> argRanges, SetIntRangeFn setResultRanges) { |
| setResultRangesForInterfaceWorkgroupOp(getResult(), getUpperBound(), |
| setResultRanges, /*minimum=*/1); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // hal.fence.* |
| //===----------------------------------------------------------------------===// |
| |
| void FenceCreateOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| setNameFn(getResult(), "fence"); |
| } |
| |
| void FenceJoinOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| setNameFn(getResult(), "fence"); |
| } |
| |
| void FenceAwaitOp::getAsmResultNames( |
| function_ref<void(Value, StringRef)> setNameFn) { |
| setNameFn(getStatus(), "status"); |
| } |
| |
| } // namespace mlir::iree_compiler::IREE::HAL |
| |
| //===----------------------------------------------------------------------===// |
| // TableGen definitions (intentionally last) |
| //===----------------------------------------------------------------------===// |
| |
| #define GET_OP_CLASSES |
| #include "iree/compiler/Dialect/HAL/IR/HALOps.cpp.inc" |