This is an archive of the discontinued LLVM Phabricator instance.

[mlir][Pass] Deprecate FunctionPass in favor of a more general SymbolDefinitionPass
AbandonedPublic

Authored by rriddle on Jan 4 2022, 3:44 PM.

Details

Summary

This revision generalizes the functionality currently present in FunctionPass, i.e.
running a pass on symbol definitions not declarations. This allows for removing the
dependency on FuncOp from Pass, and also more easily support non-FuncOp
function definition passes.

The definition of FunctionPass is left intact for now to allow time for downstream
users to migrate.

Diff Detail

Event Timeline

rriddle created this revision.Jan 4 2022, 3:44 PM
rriddle requested review of this revision.Jan 4 2022, 3:44 PM
mehdi_amini added inline comments.Jan 5 2022, 7:40 PM
mlir/docs/Rationale/Rationale.md
1057

This makes the sentence less clear to me.

What about "Constants are defined in per-region pool (for example for a given function body), ..."

mlir/lib/Dialect/Affine/Transforms/LoopTiling.cpp
41

There a bunch of these that I'm not sure why they are restricted to a Symbol operation, I think this patch may make it more apparent during code review!

bondhugula added a comment.EditedJan 6 2022, 7:10 AM

There could be many ops that define symbols while the intent of the current FunctionPass passes has been to run on declarative constructs that are function-like. The name FunctionPass can be preserved but you can just still it from FuncOp and still break the dependency on Pass, i.e., FunctionPass's new meaning could be a pass that runs on all things that are FunctionLike?

There could be many ops that define symbols while the intent of the current FunctionPass passes has been to run on declarative constructs that are function-like. The name FunctionPass can be preserved but you can just still it from FuncOp and still break the dependency on Pass, i.e., FunctionPass's new meaning could be a pass that runs on all things that are FunctionLike?

The new SymbolDefinitionPass still requires that you provide the operation that the pass should operate under, so there isn't any behavior change from what happens now. The fact that the pass operates on FuncOp will now be explicit in the definition of the pass itself, instead of implicit (FunctionPass only works on FuncOp, and not many people realize that!!). I don't see a reason to keep a FunctionPass, given that the only property is operating on definitions, which is more of a symbol thing in general. For a FunctionLike pass, I intend to add some form of support for interface/trait passes, so that would cover FunctionLike (or any other operation property) without the need to define a custom pass type.

There could be many ops that define symbols while the intent of the current FunctionPass passes has been to run on declarative constructs that are function-like. The name FunctionPass can be preserved but you can just still it from FuncOp and still break the dependency on Pass, i.e., FunctionPass's new meaning could be a pass that runs on all things that are FunctionLike?

The new SymbolDefinitionPass still requires that you provide the operation that the pass should operate under, so there isn't any behavior change from what happens now. The fact that the pass operates on FuncOp will now be explicit in the definition of the pass itself, instead of implicit (FunctionPass only works on FuncOp, and not many people realize that!!). I don't see a reason to keep a FunctionPass, given that the only property is operating on definitions, which is more of a symbol thing in general. For a FunctionLike pass, I intend to add some form of support for interface/trait passes, so that would cover FunctionLike (or any other operation property) without the need to define a custom pass type.

Right - but what I was trying to get at is whether this SymbolDefinitionPass would be called on anything other than an op with FunctionLike trait? In some way, by introducing this new name, you've provided a broader base (broader than FuncOp and FunctionLike) but narrower than an OperationPass -- do you need this abstraction at all as a special thing? For eg., I find it pretty rare that a pass type is needed for "symbol defining" ops that themselves do not have regions or that do not have the trait FunctionLike.

Should we just be directly jumping to a codebase change that changes FunctionPass to FunctionLikePass<FuncOp>? Also, wouldn't FunctionPass -> OperationPass<FuncOp> also break the Pass's dependency on FuncOp?

There could be many ops that define symbols while the intent of the current FunctionPass passes has been to run on declarative constructs that are function-like. The name FunctionPass can be preserved but you can just still it from FuncOp and still break the dependency on Pass, i.e., FunctionPass's new meaning could be a pass that runs on all things that are FunctionLike?

The new SymbolDefinitionPass still requires that you provide the operation that the pass should operate under, so there isn't any behavior change from what happens now. The fact that the pass operates on FuncOp will now be explicit in the definition of the pass itself, instead of implicit (FunctionPass only works on FuncOp, and not many people realize that!!). I don't see a reason to keep a FunctionPass, given that the only property is operating on definitions, which is more of a symbol thing in general. For a FunctionLike pass, I intend to add some form of support for interface/trait passes, so that would cover FunctionLike (or any other operation property) without the need to define a custom pass type.

Right - but what I was trying to get at is whether this SymbolDefinitionPass would be called on anything other than an op with FunctionLike trait? In some way, by introducing this new name, you've provided a broader base (broader than FuncOp and FunctionLike) but narrower than an OperationPass -- do you need this abstraction at all as a special thing? For eg., I find it pretty rare that a pass type is needed for "symbol defining" ops that themselves do not have regions or that do not have the trait FunctionLike.

Yes it would. Functions are not universal, and I've had several projects that don't use functions but still want to schedule passes on symbol definitions only. Aside from that, I don't see a reason to artificially limit something generic to "Functions" if it is still per-operation type. As an aside, having a "FunctionPass" has created a situation where a lot of passes are unnecessarily limited to functions, which is an unfortunate carry over from LLVM. I'd prefer things not to be over-specific if we don't have a reason to be.

Should we just be directly jumping to a codebase change that changes FunctionPass to FunctionLikePass<FuncOp>? Also, wouldn't FunctionPass -> OperationPass<FuncOp> also break the Pass's dependency on FuncOp?

That would result in a behavior change. Most (well probably close to all) passes running on functions (and other symbols) don't really need to operate on declarations, and doing so can often just be unnecessary compute (why go through the motions of setting up canonicalizer/cse/etc. if you aren't going to do anything?).

There could be many ops that define symbols while the intent of the current FunctionPass passes has been to run on declarative constructs that are function-like. The name FunctionPass can be preserved but you can just still it from FuncOp and still break the dependency on Pass, i.e., FunctionPass's new meaning could be a pass that runs on all things that are FunctionLike?

The new SymbolDefinitionPass still requires that you provide the operation that the pass should operate under, so there isn't any behavior change from what happens now. The fact that the pass operates on FuncOp will now be explicit in the definition of the pass itself, instead of implicit (FunctionPass only works on FuncOp, and not many people realize that!!). I don't see a reason to keep a FunctionPass, given that the only property is operating on definitions, which is more of a symbol thing in general. For a FunctionLike pass, I intend to add some form of support for interface/trait passes, so that would cover FunctionLike (or any other operation property) without the need to define a custom pass type.

Right - but what I was trying to get at is whether this SymbolDefinitionPass would be called on anything other than an op with FunctionLike trait? In some way, by introducing this new name, you've provided a broader base (broader than FuncOp and FunctionLike) but narrower than an OperationPass -- do you need this abstraction at all as a special thing? For eg., I find it pretty rare that a pass type is needed for "symbol defining" ops that themselves do not have regions or that do not have the trait FunctionLike.

Yes it would. Functions are not universal, and I've had several projects that don't use functions but still want to schedule passes on symbol definitions only. Aside from that, I don't see a reason to artificially limit something generic to "Functions" if it is still per-operation type. As an aside, having a "FunctionPass" has created a situation where a lot of passes are unnecessarily limited to functions, which is an unfortunate carry over from LLVM. I'd prefer things not to be over-specific if we don't have a reason to be.

Should we just be directly jumping to a codebase change that changes FunctionPass to FunctionLikePass<FuncOp>? Also, wouldn't FunctionPass -> OperationPass<FuncOp> also break the Pass's dependency on FuncOp?

That would result in a behavior change. Most (well probably close to all) passes running on functions (and other symbols) don't really need to operate on declarations, and doing so can often just be unnecessary compute (why go through the motions of setting up canonicalizer/cse/etc. if you aren't going to do anything?).

Not to say that we couldn't just use OperationPass, but we have to be okay with the tradeoff of now processing declarations.

bondhugula added inline comments.Jan 7 2022, 6:49 AM
mlir/include/mlir/Pass/Pass.h
387–389

Is a static_assert missing here on OpT having the Symbol trait?

There could be many ops that define symbols while the intent of the current FunctionPass passes has been to run on declarative constructs that are function-like. The name FunctionPass can be preserved but you can just still it from FuncOp and still break the dependency on Pass, i.e., FunctionPass's new meaning could be a pass that runs on all things that are FunctionLike?

The new SymbolDefinitionPass still requires that you provide the operation that the pass should operate under, so there isn't any behavior change from what happens now. The fact that the pass operates on FuncOp will now be explicit in the definition of the pass itself, instead of implicit (FunctionPass only works on FuncOp, and not many people realize that!!). I don't see a reason to keep a FunctionPass, given that the only property is operating on definitions, which is more of a symbol thing in general. For a FunctionLike pass, I intend to add some form of support for interface/trait passes, so that would cover FunctionLike (or any other operation property) without the need to define a custom pass type.

Right - but what I was trying to get at is whether this SymbolDefinitionPass would be called on anything other than an op with FunctionLike trait? In some way, by introducing this new name, you've provided a broader base (broader than FuncOp and FunctionLike) but narrower than an OperationPass -- do you need this abstraction at all as a special thing? For eg., I find it pretty rare that a pass type is needed for "symbol defining" ops that themselves do not have regions or that do not have the trait FunctionLike.

Yes it would. Functions are not universal, and I've had several projects that don't use functions but still want to schedule passes on symbol definitions only. Aside from that, I don't see a reason to artificially limit something generic to "Functions" if it is still per-operation type. As an aside, having a "FunctionPass" has created a situation where a lot of passes are unnecessarily limited to functions, which is an unfortunate carry over from LLVM. I'd prefer things not to be over-specific if we don't have a reason to be.

Should we just be directly jumping to a codebase change that changes FunctionPass to FunctionLikePass<FuncOp>? Also, wouldn't FunctionPass -> OperationPass<FuncOp> also break the Pass's dependency on FuncOp?

That would result in a behavior change. Most (well probably close to all) passes running on functions (and other symbols) don't really need to operate on declarations, and doing so can often just be unnecessary compute (why go through the motions of setting up canonicalizer/cse/etc. if you aren't going to do anything?).

Thanks. Looks like I was completely missing or ignoring the feature of being able to run on symbol/function ops that "define but not declare", and you are now providing that feature to a larger number of ops -- any symbol trait op. (This is all separate from letting passes run on all function-like ops or all symbol ops, etc.).

The "process only definition but not declaration" part is interesting/useful, however it seems not completely satisfactory to me: I think we're conflating the pass definition with the scheduling of the pass.

I mean that many passes could be "OperationPass" but we would make them "SymbolDefinitionPass" just to skip declaration, at the same time forcing a dependency on a "SymbolOp" artificially.
I think what we want in many cases is an OperationPass but have the ability to then schedule it only on the definitions, possibly with a new filtering in the pass-pipeline itself, like this: --pass-pipeline="module(_filter.def(func(mypass)))"

The "process only definition but not declaration" part is interesting/useful, however it seems not completely satisfactory to me: I think we're conflating the pass definition with the scheduling of the pass.

I mean that many passes could be "OperationPass" but we would make them "SymbolDefinitionPass" just to skip declaration, at the same time forcing a dependency on a "SymbolOp" artificially.
I think what we want in many cases is an OperationPass but have the ability to then schedule it only on the definitions, possibly with a new filtering in the pass-pipeline itself, like this: --pass-pipeline="module(_filter.def(func(mypass)))"

I agree to a certain extent, and intend to explore general scheduling things in the pass manager. Not visiting declarations almost feels intrinsic to operating on symbol operations in general, i.e. I doubt we would want to annotate every (well every existing pipeline for sure) pipeline with a wrapping filter.def. Another concern I would have about moving this to the pipeline level though, is that it kind of separates invariants about the pass from the pass itself. For example, if the pass doesn't explicitly filter out definitions itself, we would need to duplicate this via asserts/etc. in the pass (which is less than ideal). At that point, there isn't a huge benefit from the pipeline scheduling IMO.

The "process only definition but not declaration" part is interesting/useful, however it seems not completely satisfactory to me: I think we're conflating the pass definition with the scheduling of the pass.

I mean that many passes could be "OperationPass" but we would make them "SymbolDefinitionPass" just to skip declaration, at the same time forcing a dependency on a "SymbolOp" artificially.
I think what we want in many cases is an OperationPass but have the ability to then schedule it only on the definitions, possibly with a new filtering in the pass-pipeline itself, like this: --pass-pipeline="module(_filter.def(func(mypass)))"

I agree to a certain extent, and intend to explore general scheduling things in the pass manager. Not visiting declarations almost feels intrinsic to operating on symbol operations in general,

I agree, but the question is where should this information be encoded.

i.e. I doubt we would want to annotate every (well every existing pipeline for sure) pipeline with a wrapping filter.def.

I'm not sure what is the alternative though? The weirdness right now is that --pass-pipeline="func(cse, affine-loop-tiling)" will run CSE on declarations and definitions, but affine-loop-tiling on definitions only.

Another concern I would have about moving this to the pipeline level though, is that it kind of separates invariants about the pass from the pass itself.

My point is exactly that these aren't invariant of the pass when the pass has no connection to the concept of "symbol" (like affine-loop-tiling for example): why would this be an invariant of some of these pass but not CSE?

But maybe that comes back to my point inline in the review: when I'll read a pass that is runOnSymbol() in the future that may ring a bell and makes us ask "what is the fundamental connection to 'symbol' in this pass?".

The "process only definition but not declaration" part is interesting/useful, however it seems not completely satisfactory to me: I think we're conflating the pass definition with the scheduling of the pass.

I mean that many passes could be "OperationPass" but we would make them "SymbolDefinitionPass" just to skip declaration, at the same time forcing a dependency on a "SymbolOp" artificially.
I think what we want in many cases is an OperationPass but have the ability to then schedule it only on the definitions, possibly with a new filtering in the pass-pipeline itself, like this: --pass-pipeline="module(_filter.def(func(mypass)))"

I agree to a certain extent, and intend to explore general scheduling things in the pass manager. Not visiting declarations almost feels intrinsic to operating on symbol operations in general,

I agree, but the question is where should this information be encoded.

i.e. I doubt we would want to annotate every (well every existing pipeline for sure) pipeline with a wrapping filter.def.

I'm not sure what is the alternative though? The weirdness right now is that --pass-pipeline="func(cse, affine-loop-tiling)" will run CSE on declarations and definitions, but affine-loop-tiling on definitions only.

Another concern I would have about moving this to the pipeline level though, is that it kind of separates invariants about the pass from the pass itself.

My point is exactly that these aren't invariant of the pass when the pass has no connection to the concept of "symbol" (like affine-loop-tiling for example): why would this be an invariant of some of these pass but not CSE?

Right, a difference here (IMO) is that CSE is designed to be generic over any potential input. The pass (affine-loop-tiling in this example) is the one that is establishing an invariant on what it expects to run on (a definition, whether it really needs to is debatable). If a pass does not
expect a declaration, it will still need to encode this as an invariant in some way. This ties in with the invariants of what the specific pass expects to operate on, and what expectations it can assume about the input. If a pass that operates on a
FuncOp (or other function) wants to assume that the passed in operation is a definition, it needs to verify that assumption itself. Even if we had the scheduling as part of the pass manager itself, this would still be true (the pass could be nested
in a filtered pass manager, or it couldn't). We could (as Uday mentions upthread) remove this entirely and enforce that all of the current FunctionPass' properly handle declarations, and then consider the potentials of moving some invariants to
the pass manager itself.

But maybe that comes back to my point inline in the review: when I'll read a pass that is runOnSymbol() in the future that may ring a bell and makes us ask "what is the fundamental connection to 'symbol' in this pass?".

I think this is separable a bit from the discussion above. We should already be questioning a bit whether a pass truly needs to run on a FuncOp or not. Having runOnSymbol would definitely make it a bit more obvious that something
is overly constrained.

Happy to drop this and move passes to just OperationPass<FuncOp> if we would rather drive down that approach (I don't think it saves much in the way of encoding invariants, though it will move around where the encoding is actually defined).

Right, a difference here (IMO) is that CSE is designed to be generic over any potential input. The pass (affine-loop-tiling in this example) is the one that is establishing an invariant on what it expects to run on (a definition, whether it really needs to is debatable). If a pass does not
expect a declaration, it will still need to encode this as an invariant in some way.

My whole premise is that most "FunctionPass" don't need to work on Symbols: so talking about definiton/declaration is irrelevant from there.

But maybe that comes back to my point inline in the review: when I'll read a pass that is runOnSymbol() in the future that may ring a bell and makes us ask "what is the fundamental connection to 'symbol' in this pass?".

I think this is separable a bit from the discussion above. We should already be questioning a bit whether a pass truly needs to run on a FuncOp or not. Having runOnSymbol would definitely make it a bit more obvious that something
is overly constrained.

Happy to drop this and move passes to just OperationPass<FuncOp> if we would rather drive down that approach (I don't think it saves much in the way of encoding invariants, though it will move around where the encoding is actually defined).

That wasn't my point though, it is even the opposite of what I'm saying: OperationPass<FuncOp> is even more constrained than what you're doing here. My point was that most passes are like CSE: they shouldn't have any restriction.

You patch makes sense for passes that actually operates on Symbol. I guess what would be ideal here would be to not make things like affine-loop-tiling SymbolDefinitionPass but just Pass.

jpienaar added inline comments.Jan 12 2022, 6:44 PM
mlir/include/mlir/Pass/Pass.h
397

The name doesn't convey this benefit to me or invoke any intuition about what it does. It is less readable than OperationPass, makes we wonder which symbol is being talked about, and the benefit is declartion filtering (which I would not have guessed looking at the name).

The first thing I'd probably do as user is to create an alias (something like FuncOpPass = SymbolDefinition<FuncOp>, probably ODS side) and then use that so that I have a feel.

How many passes considers declarations too vs not? Could we have the more intuitive name be the more frequent one?

402

Why not mark as deprecated so that folks get a warning when compiling? (https://en.cppreference.com/w/cpp/language/attributes/deprecated)

bondhugula added inline comments.Jan 12 2022, 7:51 PM
mlir/include/mlir/Pass/Pass.h
397

I agree with Jacques here on all of this.

bondhugula requested changes to this revision.Jan 12 2022, 7:54 PM
bondhugula added inline comments.
mlir/include/mlir/Pass/Pass.h
397

something like FuncOpPass = SymbolDefinition<FuncOp>,

How about FuncDefPass = SymbolDefinition<FuncOp>?

This revision now requires changes to proceed.Jan 12 2022, 7:54 PM
rriddle added inline comments.Jan 12 2022, 7:56 PM
mlir/include/mlir/Pass/Pass.h
397

Strong -1 on any kind of alias with Func in the name. A large portion of this is to de-emphasize FuncOp, as it has no place being special cased in the infra. Users are free to create aliases as they need for their projects, but the core library shouldn't have anything like that.

I'm going to prepare another patch that just removes uses of FunctionPass altogether and encodes skipping the declaration directly. We can compare and decide if we should have a special pass to encode skipping declarations, or just rely on the user to do it in the runOnOperation+wait for better scheduling primitives at a higher level.

Alternate here: https://reviews.llvm.org/D117182. I don't have a huge preference in either direction, especially given that I want to evolve aspects of the pass manager in general, but the major aspect that I want to achieve here is killing anything resembling "FunctionPass".

rriddle abandoned this revision.Jan 13 2022, 7:25 PM

Abandoned in favor of D117182

bondhugula added inline comments.Jan 13 2022, 8:44 PM
mlir/include/mlir/Pass/Pass.h
397

using FuncDefPass = SymbolDefinition<FuncOp>

But this is a template specialized with FuncOp. If the RHS has FuncOp, it appears meaningful to have Func in the name. You need some canonical op to anchor and run passes on till a generic version of such canonical things are available.