Repository navigation
Canonicalize XLA wrapper inputs #3350
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
014404c
Canonicalize XLA wrapper inputs
vimarsh6739 0bc08d4
Use NoOffsetViewInterface for wrapper canonicalization
vimarsh6739 8a9d266
Use OffsetViewInterface for pointer conversions
vimarsh6739 bd8cad5
Support specialized inputs and metadata in wrapper canonicalization
vimarsh6739 59a440e
Deduplicate specialized XLA wrapper inputs
vimarsh6739 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ | |
| #include "Enzyme/MLIR/Dialect/Ops.h" | ||
| #include "Dialect.h" | ||
| #include "Interfaces/AutoDiffTypeInterface.h" | ||
| #include "Interfaces/Utils.h" | ||
| #include "Ops.h" | ||
| #include "src/enzyme_ad/jax/Dialect/Canonicalizers.h" | ||
| #include "src/enzyme_ad/jax/Dialect/Utils.h" | ||
|
|
@@ -2200,6 +2201,288 @@ void XLAWrapperOp::getEffects( | |
| effects.emplace_back(MemoryEffects::Effect::get<MemoryEffects::Write>()); | ||
| } | ||
|
|
||
| static bool hasXLAWrapperSignature(XLAWrapperOp wrapper, | ||
| FunctionOpInterface function) { | ||
| int64_t numSpecialized = wrapper.getNumSpecialized(); | ||
| if (numSpecialized < 0 || | ||
| numSpecialized > static_cast<int64_t>(wrapper.getInputs().size())) | ||
| return false; | ||
| unsigned numBuffers = wrapper.getInputs().size() - numSpecialized; | ||
| return function.getNumArguments() == wrapper.getInputs().size() && | ||
| function.getNumResults() == numBuffers && | ||
| function.getArgumentTypes().take_front(numBuffers) == | ||
| function.getResultTypes() && | ||
| (!wrapper.getArgAttrsAttr() || | ||
| wrapper.getArgAttrsAttr().size() == wrapper.getInputs().size()) && | ||
| (!wrapper.getResAttrsAttr() || | ||
| wrapper.getResAttrsAttr().size() == numBuffers); | ||
| } | ||
|
|
||
| static ArrayAttr removeWrapperAttrs(ArrayAttr attributes, | ||
| const llvm::BitVector &removed) { | ||
| SmallVector<Attribute> remaining; | ||
| for (auto [index, attribute] : llvm::enumerate(attributes)) | ||
| if (!removed.test(index)) | ||
| remaining.push_back(attribute); | ||
| return ArrayAttr::get(attributes.getContext(), remaining); | ||
| } | ||
|
|
||
| static void removeWrapperInputs(XLAWrapperOp wrapper, | ||
| const llvm::BitVector &arguments, | ||
| const llvm::BitVector &results) { | ||
| if (auto attributes = wrapper.getArgAttrsAttr()) | ||
| wrapper.setArgAttrsAttr(removeWrapperAttrs(attributes, arguments)); | ||
| if (auto attributes = wrapper.getResAttrsAttr()) | ||
| wrapper.setResAttrsAttr(removeWrapperAttrs(attributes, results)); | ||
| unsigned numSpecialized = | ||
| wrapper.getNumSpecialized() - (arguments.count() - results.count()); | ||
| wrapper->eraseOperands(arguments); | ||
| wrapper.setNumSpecialized(numSpecialized); | ||
| } | ||
|
|
||
| /// Remove a buffer slot if the function only returns its input unchanged. | ||
| /// Remove a specialized scalar input if the function does not use it. | ||
| /// Change a private function in place if every use supports the rewrite. | ||
| /// Otherwise, use one clone for compatible wrappers and keep other calls. | ||
| class RemoveUnusedXLAWrapperInputs final | ||
| : public OpRewritePattern<XLAWrapperOp> { | ||
| public: | ||
| using OpRewritePattern<XLAWrapperOp>::OpRewritePattern; | ||
|
|
||
| LogicalResult matchAndRewrite(XLAWrapperOp op, | ||
| PatternRewriter &rewriter) const override { | ||
| auto function = dyn_cast_or_null<FunctionOpInterface>( | ||
| SymbolTable::lookupNearestSymbolFrom(op, op.getFnAttr())); | ||
| if (!function || !function.getFunctionBody().hasOneBlock() || | ||
| !hasXLAWrapperSignature(op, function)) | ||
| return failure(); | ||
|
|
||
| // Keep relative symbol references in the cloned body in the same scope. | ||
| Operation *scope = SymbolTable::getNearestSymbolTable(op); | ||
| if (function->getParentOp() != scope) | ||
| return failure(); | ||
|
|
||
| Block &body = function.getFunctionBody().front(); | ||
| Operation *returnOp = body.getTerminator(); | ||
| unsigned numBuffers = function.getNumResults(); | ||
| if (!returnOp->hasTrait<OpTrait::ReturnLike>() || | ||
| returnOp->getNumOperands() != numBuffers) | ||
| return failure(); | ||
|
|
||
| llvm::BitVector unusedArguments(function.getNumArguments()); | ||
| llvm::BitVector unusedResults(numBuffers); | ||
| for (BlockArgument argument : body.getArguments()) { | ||
| unsigned index = argument.getArgNumber(); | ||
| if (index < numBuffers) { | ||
| if (argument.hasOneUse() && returnOp->getOperand(index) == argument) { | ||
| unusedArguments.set(index); | ||
| unusedResults.set(index); | ||
| } | ||
| } else if (argument.use_empty()) { | ||
| unusedArguments.set(index); | ||
| } | ||
| } | ||
| if (unusedArguments.none()) | ||
| return failure(); | ||
|
|
||
| Type withoutArguments = function.getTypeWithoutArgs(unusedArguments); | ||
| Type type = | ||
| function.getTypeWithoutArgsAndResults(unusedArguments, unusedResults); | ||
| if (!withoutArguments || !type) | ||
| return failure(); | ||
|
|
||
| llvm::SmallSetVector<Operation *, 4> callers; | ||
| callers.insert(op); | ||
| auto uses = SymbolTable::getSymbolUses(function.getOperation(), scope); | ||
| bool modifyInPlace = | ||
| uses.has_value() && SymbolTable::getSymbolVisibility(function) == | ||
| SymbolTable::Visibility::Private; | ||
| if (uses) { | ||
| for (const auto &use : *uses) { | ||
| auto wrapper = dyn_cast<XLAWrapperOp>(use.getUser()); | ||
| if (wrapper && wrapper.getFnAttr() == op.getFnAttr() && | ||
| hasXLAWrapperSignature(wrapper, function) && | ||
| SymbolTable::getNearestSymbolTable(wrapper) == scope) | ||
| callers.insert(wrapper); | ||
| else | ||
| modifyInPlace = false; | ||
| } | ||
| } | ||
|
|
||
| FunctionOpInterface specialized = function; | ||
| std::string name = function.getName().str(); | ||
| if (!modifyInPlace) { | ||
| SymbolTable symbols(scope); | ||
| std::string stem = (function.getName() + "_without_unused").str(); | ||
| name = stem; | ||
| for (unsigned suffix = 0; symbols.lookup(name); ++suffix) | ||
| name = stem + "_" + std::to_string(suffix); | ||
|
|
||
| Operation *clone = function->clone(); | ||
| specialized = cast<FunctionOpInterface>(clone); | ||
| SymbolTable::setSymbolName(clone, name); | ||
| SymbolTable::setSymbolVisibility(clone, SymbolTable::Visibility::Private); | ||
| OpBuilder::InsertionGuard guard(rewriter); | ||
| rewriter.setInsertionPointAfter(function); | ||
| rewriter.insert(clone); | ||
| } | ||
|
|
||
| rewriter.startOpModification(specialized); | ||
| Operation *specializedReturn = | ||
| specialized.getFunctionBody().front().getTerminator(); | ||
| rewriter.startOpModification(specializedReturn); | ||
| specializedReturn->eraseOperands(unusedResults); | ||
| function_interface_impl::eraseFunctionArguments( | ||
| specialized, unusedArguments, withoutArguments); | ||
| function_interface_impl::eraseFunctionResults(specialized, unusedResults, | ||
| type); | ||
| rewriter.finalizeOpModification(specializedReturn); | ||
| rewriter.finalizeOpModification(specialized); | ||
|
|
||
| for (Operation *caller : callers) { | ||
| auto wrapper = cast<XLAWrapperOp>(caller); | ||
| rewriter.startOpModification(wrapper); | ||
| removeWrapperInputs(wrapper, unusedArguments, unusedResults); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do you have a test case also doing a removal of a specialized input? |
||
| wrapper.setFnAttr(FlatSymbolRefAttr::get(op.getContext(), name)); | ||
| rewriter.finalizeOpModification(wrapper); | ||
| } | ||
| return success(); | ||
| } | ||
| }; | ||
|
|
||
| /// Merge equal buffer inputs only when their returned values and attributes | ||
| /// also agree. Merge equal specialized scalar inputs if their attributes agree. | ||
| /// Clone the function because other calls can pass distinct inputs. | ||
| class DeduplicateXLAWrapperInputs final | ||
| : public OpRewritePattern<XLAWrapperOp> { | ||
| static Value canonicalResult(Value result, Block &body, | ||
| ArrayRef<unsigned> representatives) { | ||
| if (auto argument = dyn_cast<BlockArgument>(result); | ||
| argument && argument.getOwner() == &body) | ||
| return body.getArgument(representatives[argument.getArgNumber()]); | ||
| return result; | ||
| } | ||
|
|
||
| static bool sameAttributes(ArrayAttr attributes, unsigned first, | ||
| unsigned second) { | ||
| return !attributes || attributes[first] == attributes[second]; | ||
| } | ||
|
|
||
| public: | ||
| using OpRewritePattern<XLAWrapperOp>::OpRewritePattern; | ||
|
|
||
| LogicalResult matchAndRewrite(XLAWrapperOp op, | ||
| PatternRewriter &rewriter) const override { | ||
| auto function = dyn_cast_or_null<FunctionOpInterface>( | ||
| SymbolTable::lookupNearestSymbolFrom(op, op.getFnAttr())); | ||
| if (!function || !function.getFunctionBody().hasOneBlock() || | ||
| !hasXLAWrapperSignature(op, function)) | ||
| return failure(); | ||
|
|
||
| Operation *scope = SymbolTable::getNearestSymbolTable(op); | ||
| if (function->getParentOp() != scope) | ||
| return failure(); | ||
|
|
||
| Block &body = function.getFunctionBody().front(); | ||
| Operation *returnOp = body.getTerminator(); | ||
| unsigned numBuffers = function.getNumResults(); | ||
| if (!returnOp->hasTrait<OpTrait::ReturnLike>() || | ||
| returnOp->getNumOperands() != numBuffers) | ||
| return failure(); | ||
|
|
||
| DenseMap<Value, unsigned> identities; | ||
| SmallVector<unsigned> representatives; | ||
| llvm::BitVector duplicateArguments(function.getNumArguments()); | ||
| llvm::BitVector duplicateResults(numBuffers); | ||
| for (auto [index, input] : llvm::enumerate(op.getInputs())) { | ||
| bool isBuffer = index < numBuffers; | ||
| // Match scalars by SSA value. Keep buffer addresses separate. | ||
| if (index == numBuffers) | ||
| identities.clear(); | ||
| Value identity = | ||
| isBuffer | ||
| ? enzyme::oputils::getBaseObject(input, /*offsetAllowed=*/false) | ||
| : input; | ||
| auto [entry, inserted] = identities.try_emplace(identity, index); | ||
| unsigned representative = entry->second; | ||
| representatives.push_back(index); | ||
| if (inserted) | ||
| continue; | ||
| if (input.getType() != op.getInputs()[representative].getType() || | ||
| function.getArgumentTypes()[index] != | ||
| function.getArgumentTypes()[representative] || | ||
| !sameAttributes(op.getArgAttrsAttr(), index, representative) || | ||
| !sameAttributes(function.getAllArgAttrs(), index, representative)) { | ||
| if (isBuffer) | ||
| return failure(); | ||
| continue; | ||
| } | ||
| if (isBuffer && | ||
| (!sameAttributes(op.getResAttrsAttr(), index, representative) || | ||
| !sameAttributes(function.getAllResultAttrs(), index, | ||
| representative))) | ||
| return failure(); | ||
| representatives.back() = representative; | ||
| duplicateArguments.set(index); | ||
| if (isBuffer) | ||
| duplicateResults.set(index); | ||
| } | ||
| if (duplicateArguments.none()) | ||
| return failure(); | ||
|
|
||
| for (auto [index, representative] : | ||
| llvm::enumerate(ArrayRef(representatives).take_front(numBuffers))) | ||
| if (canonicalResult(returnOp->getOperand(index), body, representatives) != | ||
| canonicalResult(returnOp->getOperand(representative), body, | ||
| representatives)) | ||
| return failure(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same comment of do you have a deduplicate specialized test? |
||
|
|
||
| Type withoutArguments = function.getTypeWithoutArgs(duplicateArguments); | ||
| Type type = function.getTypeWithoutArgsAndResults(duplicateArguments, | ||
| duplicateResults); | ||
| if (!withoutArguments || !type) | ||
| return failure(); | ||
|
|
||
| SymbolTable symbols(scope); | ||
| std::string stem = (function.getName() + "_without_duplicates").str(); | ||
| std::string name = stem; | ||
| for (unsigned suffix = 0; symbols.lookup(name); ++suffix) | ||
| name = stem + "_" + std::to_string(suffix); | ||
|
|
||
| Operation *clone = function->clone(); | ||
| auto specialized = cast<FunctionOpInterface>(clone); | ||
| Block &specializedBody = specialized.getFunctionBody().front(); | ||
| for (auto [index, representative] : llvm::enumerate(representatives)) | ||
| if (duplicateArguments.test(index)) | ||
| specializedBody.getArgument(index).replaceAllUsesWith( | ||
| specializedBody.getArgument(representative)); | ||
| specializedBody.getTerminator()->eraseOperands(duplicateResults); | ||
| function_interface_impl::eraseFunctionArguments( | ||
| specialized, duplicateArguments, withoutArguments); | ||
| function_interface_impl::eraseFunctionResults(specialized, duplicateResults, | ||
| type); | ||
| SymbolTable::setSymbolName(clone, name); | ||
| SymbolTable::setSymbolVisibility(clone, SymbolTable::Visibility::Private); | ||
| { | ||
| OpBuilder::InsertionGuard guard(rewriter); | ||
| rewriter.setInsertionPointAfter(function); | ||
| rewriter.insert(clone); | ||
| } | ||
|
|
||
| rewriter.startOpModification(op); | ||
| removeWrapperInputs(op, duplicateArguments, duplicateResults); | ||
| op.setFnAttr(FlatSymbolRefAttr::get(op.getContext(), name)); | ||
| rewriter.finalizeOpModification(op); | ||
| return success(); | ||
| } | ||
| }; | ||
|
|
||
| void XLAWrapperOp::getCanonicalizationPatterns(RewritePatternSet &results, | ||
| MLIRContext *context) { | ||
| results.insert<RemoveUnusedXLAWrapperInputs, DeduplicateXLAWrapperInputs>( | ||
| context); | ||
| } | ||
|
|
||
| //===----------------------------------------------------------------------===// | ||
| // AlternativesOp | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.