| // Copyright 2026 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 "compiler/plugins/input/Torch/InputConversion/Passes.h" | |
| #include "iree/compiler/Dialect/Util/IR/UtilDialect.h" | |
| #include "iree/compiler/Dialect/Util/IR/UtilOps.h" | |
| #include "mlir/Dialect/Func/IR/FuncOps.h" | |
| #include "mlir/IR/BuiltinOps.h" | |
| #include "torch-mlir/Dialect/Torch/IR/TorchTypes.h" | |
| #include "torch-mlir/Dialect/TorchConversion/IR/TorchConversionDialect.h" | |
| #include "torch-mlir/Dialect/TorchConversion/IR/TorchConversionOps.h" | |
| namespace Torch = mlir::torch::Torch; | |
| namespace TorchConversion = mlir::torch::TorchConversion; | |
| namespace mlir::iree_compiler::TorchInput { | |
| #define GEN_PASS_DEF_SYNCFUNCCONVERSIONPASS | |
| #include "compiler/plugins/input/Torch/InputConversion/Passes.h.inc" | |
| namespace { | |
| //===----------------------------------------------------------------------===// | |
| // Synchronous-only variant of FuncConversion (see FuncConversion.cpp for the | |
| // coarse-fences ABI it is an alternative to). Functions are rewritten in | |
| // place to a plain builtin tensor ABI intended for synchronous invocation | |
| // (e.g. via the inline HAL): immutable tensors become builtin tensors and | |
| // torch primitives their builtin scalar equivalents, with torch_c conversion | |
| // ops materialized at the edges. No fences, no HAL imports/exports, and no | |
| // $async variant are created. Mutable tensors require in-place aliasing | |
| // through the HAL and are rejected. | |
| //===----------------------------------------------------------------------===// | |
| TensorType convertToBuiltinTensorType(OpBuilder &builder, | |
| Torch::ValueTensorType vtensorType) { | |
| TensorType builtinType = vtensorType.toBuiltinTensor(); | |
| if (auto intTy = dyn_cast<IntegerType>(builtinType.getElementType())) { | |
| builtinType = builtinType.clone( | |
| builder.getIntegerType(intTy.getIntOrFloatBitWidth())); | |
| } | |
| return builtinType; | |
| } | |
| Value convertToBuiltinTensor(OpBuilder &builder, Value possibleTorchTensor) { | |
| Type ty = possibleTorchTensor.getType(); | |
| if (isa<TensorType>(ty)) { | |
| return possibleTorchTensor; | |
| } | |
| if (auto defining = dyn_cast_if_present<TorchConversion::FromBuiltinTensorOp>( | |
| possibleTorchTensor.getDefiningOp())) { | |
| return defining.getOperand(); | |
| } | |
| TensorType builtinType = | |
| convertToBuiltinTensorType(builder, cast<Torch::ValueTensorType>(ty)); | |
| return TorchConversion::ToBuiltinTensorOp::create( | |
| builder, possibleTorchTensor.getLoc(), builtinType, possibleTorchTensor); | |
| } | |
| enum class TypeDisposition { | |
| IMMUTABLE_TENSOR, | |
| TORCH_PRIMITIVE, | |
| PASSTHROUGH, | |
| }; | |
| struct ConvertedSyncFunctionInfo { | |
| IREE::Util::FuncOp funcOp; | |
| SmallVector<IREE::Util::ReturnOp> returnOps; | |
| SmallVector<Type> torchInputTypes; | |
| SmallVector<Type> torchResultTypes; | |
| SmallVector<TypeDisposition> inputDispositions; | |
| SmallVector<TypeDisposition> resultDispositions; | |
| LogicalResult postProcess(); | |
| LogicalResult convertImmutableTensorArg(BlockArgument argValue, | |
| Type torchType, OpBuilder &builder); | |
| }; | |
| LogicalResult ConvertedSyncFunctionInfo::postProcess() { | |
| if (funcOp.isExternal()) { | |
| return success(); | |
| } | |
| if (returnOps.size() != 1) { | |
| // Multi-exit funcs could be supported by materializing the result | |
| // conversions at each return; restricted to single-exit for parity with | |
| // the coarse-fences conversion. | |
| return emitError(funcOp.getLoc()) | |
| << "currently only single exit torch funcs are supported"; | |
| } | |
| Block *entryBlock = &funcOp.getBlocks().front(); | |
| // Materialize argument conversions. | |
| OpBuilder preambleBuilder = OpBuilder::atBlockBegin(entryBlock); | |
| auto entryArgs = entryBlock->getArguments(); | |
| for (auto [disp, argValue, torchType] : | |
| llvm::zip_equal(inputDispositions, entryArgs, torchInputTypes)) { | |
| switch (disp) { | |
| case TypeDisposition::IMMUTABLE_TENSOR: { | |
| if (failed(convertImmutableTensorArg(argValue, torchType, | |
| preambleBuilder))) { | |
| return failure(); | |
| } | |
| break; | |
| } | |
| case TypeDisposition::TORCH_PRIMITIVE: { | |
| Location loc = argValue.getLoc(); | |
| Operation *convertUser = nullptr; | |
| Value convertResult; | |
| if (isa<Torch::BoolType>(torchType)) { | |
| convertUser = | |
| TorchConversion::FromI1Op::create(preambleBuilder, loc, argValue); | |
| convertResult = convertUser->getResult(0); | |
| } else if (isa<Torch::FloatType>(torchType)) { | |
| convertUser = | |
| TorchConversion::FromF64Op::create(preambleBuilder, loc, argValue); | |
| convertResult = convertUser->getResult(0); | |
| } else if (isa<Torch::IntType>(torchType)) { | |
| convertUser = | |
| TorchConversion::FromI64Op::create(preambleBuilder, loc, argValue); | |
| convertResult = convertUser->getResult(0); | |
| } else { | |
| emitError(loc) << "unhandled torch primitive materialization: " | |
| << torchType; | |
| return failure(); | |
| } | |
| argValue.replaceAllUsesExcept(convertResult, convertUser); | |
| break; | |
| } | |
| case TypeDisposition::PASSTHROUGH: | |
| // Do nothing. | |
| break; | |
| } | |
| } | |
| // Materialize result conversions. | |
| IREE::Util::ReturnOp returnOp = returnOps.front(); | |
| SmallVector<Value> newReturnOperands; | |
| OpBuilder postambleBuilder(returnOp); | |
| for (auto [disp, returnValue, torchType] : llvm::zip_equal( | |
| resultDispositions, returnOp.getOperands(), torchResultTypes)) { | |
| newReturnOperands.emplace_back(returnValue); | |
| switch (disp) { | |
| case TypeDisposition::IMMUTABLE_TENSOR: { | |
| newReturnOperands.back() = | |
| convertToBuiltinTensor(postambleBuilder, returnValue); | |
| break; | |
| } | |
| case TypeDisposition::TORCH_PRIMITIVE: { | |
| Location loc = returnValue.getLoc(); | |
| if (isa<Torch::BoolType>(torchType)) { | |
| newReturnOperands.back() = | |
| TorchConversion::ToI1Op::create(postambleBuilder, loc, returnValue); | |
| } else if (isa<Torch::FloatType>(torchType)) { | |
| newReturnOperands.back() = TorchConversion::ToF64Op::create( | |
| postambleBuilder, loc, returnValue); | |
| } else if (isa<Torch::IntType>(torchType)) { | |
| newReturnOperands.back() = TorchConversion::ToI64Op::create( | |
| postambleBuilder, loc, returnValue); | |
| } else if (isa<Torch::GeneratorType>(torchType)) { | |
| newReturnOperands.back() = TorchConversion::GeneratorToI64Op::create( | |
| postambleBuilder, loc, returnValue); | |
| } else { | |
| emitError(loc) << "unhandled torch primitive materialization: " | |
| << torchType; | |
| return failure(); | |
| } | |
| break; | |
| } | |
| case TypeDisposition::PASSTHROUGH: | |
| // Do nothing. | |
| break; | |
| } | |
| } | |
| returnOp->setOperands(newReturnOperands); | |
| return success(); | |
| } | |
| LogicalResult ConvertedSyncFunctionInfo::convertImmutableTensorArg( | |
| BlockArgument argValue, Type torchType, OpBuilder &builder) { | |
| // Already a builtin tensor: nothing to materialize. | |
| if (isa<TensorType>(torchType)) { | |
| return success(); | |
| } | |
| if (!isa<Torch::ValueTensorType>(torchType)) { | |
| return emitError(argValue.getLoc()) | |
| << "unsupported immutable tensor argument: " << torchType; | |
| } | |
| // If the arg is just directly returned, then don't do anything special with | |
| // it: the postamble passes it through unconverted. | |
| bool hasNonTrivialUse = false; | |
| for (auto *userOp : argValue.getUsers()) { | |
| if (isa<IREE::Util::ReturnOp>(userOp)) { | |
| continue; | |
| } | |
| hasNonTrivialUse = true; | |
| } | |
| if (!hasNonTrivialUse) { | |
| return success(); | |
| } | |
| Value converted = TorchConversion::FromBuiltinTensorOp::create( | |
| builder, argValue.getLoc(), torchType, argValue); | |
| argValue.replaceAllUsesExcept(converted, converted.getDefiningOp()); | |
| return success(); | |
| } | |
| void retainFunctionAttributes(Operation *srcOp, IREE::Util::FuncOp destOp) { | |
| // Allowlist of function attributes to retain when importing funcs. | |
| constexpr const char *kRetainedAttributes[] = { | |
| "iree.reflection", | |
| }; | |
| for (const char *attrName : kRetainedAttributes) { | |
| if (Attribute attr = srcOp->getAttr(attrName)) { | |
| destOp->setAttr(attrName, attr); | |
| } | |
| } | |
| } | |
| class SyncFuncConversionPass final | |
| : public impl::SyncFuncConversionPassBase<SyncFuncConversionPass> { | |
| public: | |
| void getDependentDialects(DialectRegistry ®istry) const override { | |
| registry.insert<IREE::Util::UtilDialect>(); | |
| registry.insert<TorchConversion::TorchConversionDialect>(); | |
| } | |
| void runOnOperation() override { | |
| auto moduleOp = getOperation(); | |
| // Convert all functions in the module to IREE funcs. In this stage, | |
| // we convert contained return ops and argument/result types, but we have | |
| // not yet converted anything "on the inside". Therefore, it is pretty | |
| // likely the functions are still illegal. | |
| SmallVector<Operation *> eraseFuncOps; | |
| std::vector<ConvertedSyncFunctionInfo> convertedFuncInfos; | |
| for (auto funcOp : moduleOp.getOps<func::FuncOp>()) { | |
| if (!shouldConvertFunc(funcOp)) { | |
| continue; | |
| } | |
| ConvertedSyncFunctionInfo &convertedFuncInfo = | |
| convertedFuncInfos.emplace_back(); | |
| if (failed(convertFuncOp(funcOp, convertedFuncInfo))) { | |
| signalPassFailure(); | |
| return; | |
| } | |
| eraseFuncOps.push_back(funcOp); | |
| } | |
| for (auto op : eraseFuncOps) { | |
| op->erase(); | |
| } | |
| for (auto &info : convertedFuncInfos) { | |
| if (failed(info.postProcess())) { | |
| signalPassFailure(); | |
| return; | |
| } | |
| } | |
| } | |
| bool shouldConvertFunc(func::FuncOp torchFunc) { | |
| // For now, we don't touch externals and assume they are in the proper | |
| // calling convention. In the future, we may support "torch externals" | |
| // which we convert to mate up with a torch module. We can remove/adapt | |
| // this when that is elaborated. | |
| if (torchFunc.isExternal()) { | |
| return false; | |
| } | |
| // Something has already converted this and told us not to touch it. | |
| if (torchFunc->hasAttr("iree.abi.stub")) { | |
| return false; | |
| } | |
| return true; | |
| } | |
| LogicalResult convertFuncOp(func::FuncOp torchFunc, | |
| ConvertedSyncFunctionInfo &convertedFuncInfo) { | |
| IRRewriter rewriter(torchFunc.getContext()); | |
| rewriter.setInsertionPoint(torchFunc); | |
| Location loc = torchFunc.getLoc(); | |
| // Convert function signature. | |
| FunctionType torchFuncType = torchFunc.getFunctionType(); | |
| convertedFuncInfo.torchInputTypes.append(torchFuncType.getInputs().begin(), | |
| torchFuncType.getInputs().end()); | |
| convertedFuncInfo.torchResultTypes.append( | |
| torchFuncType.getResults().begin(), torchFuncType.getResults().end()); | |
| SmallVector<Type> ireeInputTypes(convertedFuncInfo.torchInputTypes); | |
| SmallVector<Type> ireeResultTypes(convertedFuncInfo.torchResultTypes); | |
| convertedFuncInfo.inputDispositions.resize(ireeInputTypes.size()); | |
| convertedFuncInfo.resultDispositions.resize(ireeResultTypes.size()); | |
| for (size_t i = 0; i < convertedFuncInfo.torchInputTypes.size(); ++i) { | |
| if (failed(convertType( | |
| rewriter, loc, convertedFuncInfo.torchInputTypes[i], | |
| ireeInputTypes[i], convertedFuncInfo.inputDispositions[i]))) { | |
| return failure(); | |
| } | |
| } | |
| for (size_t i = 0; i < convertedFuncInfo.torchResultTypes.size(); ++i) { | |
| if (failed(convertType( | |
| rewriter, loc, convertedFuncInfo.torchResultTypes[i], | |
| ireeResultTypes[i], convertedFuncInfo.resultDispositions[i]))) { | |
| return failure(); | |
| } | |
| } | |
| // Build tied operands index mapping results back to operands. | |
| SmallVector<int64_t> tiedOperands; | |
| bool anyTiedOperands = false; | |
| for (unsigned i = 0; i < torchFuncType.getNumResults(); ++i) { | |
| auto tiedAttr = | |
| torchFunc.getResultAttrOfType<IntegerAttr>(i, "iree.abi.tied"); | |
| if (tiedAttr) { | |
| tiedOperands.push_back(tiedAttr.getInt()); | |
| anyTiedOperands = true; | |
| } else { | |
| tiedOperands.push_back(-1); | |
| } | |
| } | |
| auto tiedOperandsAttr = anyTiedOperands | |
| ? rewriter.getIndexArrayAttr(tiedOperands) | |
| : ArrayAttr{}; | |
| // Create new func with the original name. | |
| FunctionType syncFuncType = | |
| FunctionType::get(loc.getContext(), ireeInputTypes, ireeResultTypes); | |
| auto syncFuncOp = IREE::Util::FuncOp::create( | |
| rewriter, torchFunc.getLoc(), torchFunc.getName(), syncFuncType, | |
| tiedOperandsAttr); | |
| convertedFuncInfo.funcOp = syncFuncOp; | |
| syncFuncOp.setSymVisibilityAttr(torchFunc.getSymVisibilityAttr()); | |
| retainFunctionAttributes(torchFunc, syncFuncOp); | |
| if (auto affinityAttr = torchFunc->getAttr("iree.abi.affinity")) { | |
| syncFuncOp->setAttr("iree.abi.affinity", affinityAttr); | |
| } | |
| rewriter.inlineRegionBefore(torchFunc.getBody(), | |
| syncFuncOp.getFunctionBody(), syncFuncOp.end()); | |
| // Convert block arguments. | |
| Block *entryBlock = &syncFuncOp.getBlocks().front(); | |
| for (size_t i = 0; i < ireeInputTypes.size(); ++i) { | |
| entryBlock->getArgument(i).setType(ireeInputTypes[i]); | |
| } | |
| // Replace return ops. | |
| syncFuncOp->walk([&](func::ReturnOp returnOp) { | |
| rewriter.setInsertionPoint(returnOp); | |
| auto ireeReturnOp = rewriter.replaceOpWithNewOp<IREE::Util::ReturnOp>( | |
| returnOp, returnOp.getOperands()); | |
| convertedFuncInfo.returnOps.push_back(ireeReturnOp); | |
| }); | |
| return success(); | |
| } | |
| LogicalResult convertType(OpBuilder &builder, Location loc, Type torchType, | |
| Type &ireeType, TypeDisposition &disp) { | |
| if (isa<TensorType>(torchType)) { | |
| ireeType = torchType; | |
| disp = TypeDisposition::IMMUTABLE_TENSOR; | |
| return success(); | |
| } | |
| if (auto vtType = dyn_cast<Torch::ValueTensorType>(torchType)) { | |
| ireeType = convertToBuiltinTensorType(builder, vtType); | |
| disp = TypeDisposition::IMMUTABLE_TENSOR; | |
| return success(); | |
| } | |
| if (isa<Torch::NonValueTensorType>(torchType)) { | |
| return emitError(loc) | |
| << "mutable tensors are not supported by sync func conversion: " | |
| << torchType; | |
| } | |
| if (isa<Torch::BoolType>(torchType)) { | |
| ireeType = IntegerType::get(torchType.getContext(), 1); | |
| disp = TypeDisposition::TORCH_PRIMITIVE; | |
| return success(); | |
| } | |
| if (isa<Torch::IntType, Torch::GeneratorType>(torchType)) { | |
| ireeType = IntegerType::get(torchType.getContext(), 64); | |
| disp = TypeDisposition::TORCH_PRIMITIVE; | |
| return success(); | |
| } | |
| if (isa<Torch::FloatType>(torchType)) { | |
| ireeType = Float64Type::get(torchType.getContext()); | |
| disp = TypeDisposition::TORCH_PRIMITIVE; | |
| return success(); | |
| } | |
| if (isa<IntegerType, FloatType, IndexType>(torchType)) { | |
| ireeType = torchType; | |
| disp = TypeDisposition::PASSTHROUGH; | |
| return success(); | |
| } | |
| return emitError(loc) << "unhandled torch type: " << torchType; | |
| } | |
| }; | |
| } // namespace | |
| } // namespace mlir::iree_compiler::TorchInput |