This is the counterpart to the forward dense dataflow analysis and
integrates into the dataflow framework. The implementation follows the
structure of existing dataflow analyses.
Details
Diff Detail
- Repository
- rG LLVM Github Monorepo
Event Timeline
mlir/lib/Analysis/DataFlow/DenseAnalysis.cpp | ||
---|---|---|
207 | How does the behaviour of linalg.generic differ from the interface here? |
Thanks! Let me go over this in more detail tonight.
mlir/include/mlir/Analysis/DataFlow/DenseAnalysis.h | ||
---|---|---|
228 | it |
mlir/lib/Analysis/DataFlow/DenseAnalysis.cpp | ||
---|---|---|
207 | linalg.generic has its own semantics, specifically read/write to memref operands, and not just forwards control to its body. So the analysis should visit it as a regular operation in addition to following the control flow. |
mlir/include/mlir/Analysis/DataFlow/DenseAnalysis.h | ||
---|---|---|
64 | Should we rename to AbstractDenseForwardDataFlowAnalysis? If you are concerned about backward compatibility, you can add: using AbstractDenseDataFlowAnalysis = AbstractDenseForwardDataFlowAnalysis; | |
91–92 | Note that when you "visit" a program point, you usually don't update the state on that program point itself. Conceptually, this dataflow framework attaches states to "nodes" and visits "edges". For example, in a sparse analysis, you visit an op: %result = some_op (%arg1, %arg2) During the visit, you read the states on %arg1 and %arg2, and write the state on %result. You are not "updating" any state on some_op itself. | |
mlir/lib/Analysis/DataFlow/DenseAnalysis.cpp | ||
203 | I'm a bit concerned about the design here: In a dense forward analysis, getLattice(op) means the lattice element after op, but here it means the lattice element before op. This inconsistency could lead to confusion when people read the results of multiple analyses. For example, live range analysis depends on reaching definition analysis (forward) and live variable analysis (backward). It could be weird if, in different analyses, getOrCreate<T>(op) loads lattice elements from different locations. Should we make it consistent that getLattice(op) means after op, and getLattice(block) means at the entry of block? | |
284 | I'm curious: why are we using meet in backward analysis and join in forward analysis? I tend to think that we can just use join in both cases. I see that https://en.wikipedia.org/wiki/Join_and_meet says join and meet go in opposite directions, but it seems that we only need this separation if a lattice needs to define join and meet at the same time. But I think that in each analysis, we only need one kind of merge, right? |
mlir/lib/Analysis/DataFlow/DenseAnalysis.cpp | ||
---|---|---|
203 | It's contextual to the analysis what getLattice means. It could alleviate confusion to name them getLatticeAfter and getLatticeBefore respectively. |
Fix nitpicks.
mlir/include/mlir/Analysis/DataFlow/DenseAnalysis.h | ||
---|---|---|
64 | Not in this patch, renamings are much better off as a standalone patch. And the change, if made, should also affect the sparse analyses that have the same duality. I don't have any problem with the existing naming convention, by the way. | |
91–92 | I don't see how the suggested change reflects the associated comment. Both function arguments are program points. The comment argues that program points are "visited" and not "updated". The original commit uses "updated" consistently for both. The proposed change uses "visited" for one and "updated" for another, adding more terminology without removing the existing one. This only adds complexity and mental load and no clear value. The documentation repeatedly states that state is attached to program points, e.g. https://github.com/llvm/llvm-project/blob/main/mlir/include/mlir/Analysis/DataFlowFramework.h#L55-L57, https://github.com/llvm/llvm-project/blob/main/mlir/include/mlir/Analysis/DataFlowFramework.h#L62-L63, https://github.com/llvm/llvm-project/blob/main/mlir/include/mlir/Analysis/DataFlowFramework.h#L264-L265, etc. So it doesn't sound right to start introducing some distinction at this level. It sounds a bit of a mouthful to say, but maybe "analysis state associated with program point foo" would be clearer here. | |
mlir/lib/Analysis/DataFlow/DenseAnalysis.cpp | ||
203 | The meaning of a lattice for a specific analysis is documented by the analysis. A lattice is defined for a program point. Program points are not limited to operations and blocks, so the API shouldn't over-optimize for those. Even for the operation and block points, the "before"/"after" distinction would be require using opposite calls: in forward analyses, one would want a getLatticeBefore(block) and a getLatticeAfter(op) since getLatticeAfter(block) cannot be clearly associated with the state at the beginning of the block. Since both directions will need to provide both "before" and 'after", the potential for misuse isn't really removed. For lattices associated with values or user-defined points, it's even less clear what "before" and 'after" would mean. | |
284 | For consistency with the sparse analysis. The way that both analysis are currently written doesn't seem to differentiate between the two operations, and will likely have to be updated to support them properly. |
mlir/include/mlir/Analysis/DataFlow/DenseAnalysis.h | ||
---|---|---|
91–92 | Sorry for the confusion. The difference in the suggested change is subtle in dense analyses, but more obvious in sparse analyses. In a sparse analysis, we attach lattices to mlir::Values and update these lattices, but visit mlir::Operations. In SparseAnalysis.h, visitOperation looks like this: template <typename StateT> class SparseDataFlowAnalysis : public AbstractSparseDataFlowAnalysis { ... virtual void visitOperation(Operation *op, ArrayRef<const StateT *> operands, ArrayRef<StateT *> results) = 0; ... }; The op was inserted in the worklist and visited, but operands and results are lattices attached to mlir::Values and those are updated during the visit. Then, when a lattice on a mlir::Value is updated, all ops that use this mlir::Value are inserted to the worklist and visited. But the ops themselves are not "updated". Both mlir::Operation * and mlir::Value are ProgramPoints. Going back to dense analyses. Conceptually and theoretically speaking, when we attach lattices to mlir::Operation *s, we are not really attaching lattices onto the ops themselves, but before and after ops. This is also what the comment says ("Get the dense lattice after the execution of the given program point"). When we visit an mlir::Operation *, we are updating lattices before / after this op, but not the on the op itself. Therefore, in a forward dense analysis, the full description is that:
Apologize for being too pedantic here. The comment here doesn't warrant so much attention, but I do want to note that in our past experience in using the dataflow API, we got confused by how to understand what are being visited and where are lattices attached, and after spending a lot of time, we found that the "node/edge" way of thinking helped a lot (this also fits the concept of transfer function - an op transfers from some input state(s) to some output state(s)). I think the bigger issue is that ProgramPoint is very generic and used as both nodes (where we attach lattices) and edges (where we define transfer functions). This distinction exists throughout the framework but it's not explicitly spelled out in the type(s). | |
mlir/lib/Analysis/DataFlow/DenseAnalysis.cpp | ||
203 | Sounds good, let's not change in this patch then. Just a quick note: in our project, we have a wrapper of this API that provides four functions: GetStateBefore(op), GetStateAfter(op), GetStateAtEntryOf(block), GetStateAtEndOf(block) just to eliminate confusions. | |
284 | Sounds good. Let's not change this patch then. I kind of think that meet is unnecessary in sparse analysis as well and should be removed, but that's clearly out of the scope of this patch. |
mlir/include/mlir/Analysis/DataFlow/DenseAnalysis.h | ||
---|---|---|
91–92 | Thanks for the detailed write-up! FWIW, it wasn't obvious for me what depends on what by reading the comment. I've put your full description into the comment for more clarity. I can see the node/edge model and how it helps reasoning about regular dataflow analyses. MLIR needs to be very open as infrastructure, hence the notion of ProgramPoint that is extensible. I *think* the top-level data flow framework only reasons about those and, if we wanted to differentiate nodes and edges, we'd have to do it at that level. My current mental model is that only nodes, i.e. ProgramPoints matter. A lattice is associated with a program point. The framework visits program points: https://github.com/llvm/llvm-project/blob/425c57489283adcaa28e916c5b1c2826e7a0e042/mlir/include/mlir/Analysis/DataFlowFramework.h#L384. There are also dependencies between essentially pairs of (program point, lattice) so when a lattice is updated, the dependent program points need to be (re)visited and the corresponding lattices updated. But I don't really dissociate lattices and program points at that level. It's specific to an analysis and a program point that the lattice describes the state before or after a program point. Where it gets tricky is that we may have program points that don't have a lattice associated (operations, blocks, and generic points for the sparse case) as well as program points that have a lattice associated but not directly visited (values in the sparse case). Furthermore, visiting a program point does not necessarily update the lattice associated with that point (which is reasonable given that it may be in a state where an update is no longer possible). What's worse, visiting a program point may update the lattices associated with *other* program points. This is the case for sparse analysis where visiting an operation or a block point updates the lattices for values. Maybe this is where the dissociation should happen and we should somehow indicate which kinds of program points are "visitable" and which kinds have lattices associated. | |
mlir/lib/Analysis/DataFlow/DenseAnalysis.cpp | ||
203 | These sounds helpful as an addition. I suppose there is a way to implement them generically by checking if the state is associated with a forward or with a backward analysis. Any chance of upstreaming these? | |
284 | I was thinking about making meet actually different from join in a correct way, but this probably needs a case where a lattice is used in both directions that I don't have. There is a related issue. What currently happens is that the analysis will reset the lattice to the entry (for forward) or exit (for backward) state when it hits an ununalizable case. This implies that we start in the "bottom" state for joins (respectively in the" top" state for meets) so the following join (meet) always takes the other value. I suppose there are cases where we will need to differentiate the entry (exit) state and the state after hitting the unanalyzable case. That is, start in the "bottom" ("top") state and set to "top" ("bottom") state when using join (meet). For the propagation itself, it looks like having only "join" is sufficient, but for consistency checks of the lattice we may want "meet". WDYT? We can start with removing "meet" from both and see where it goes from there. |
Should we rename to AbstractDenseForwardDataFlowAnalysis? If you are concerned about backward compatibility, you can add: