Using a dialect to represent analysis information

Hello!
I’m curious to know if anybody has tried to use a dialect to represent the analysis information of another dialect that describes the computation. An example that comes to mind is Scalar Evolution analysis, where we could have a SCEV dialect to represent the SCEV expressions for a given function written in SCF+Standard dialects (btw, this is just an example, I’m not trying to implement SE :slight_smile:).

I think using a dialect to represent analysis information could be interesting because we could implement passes to incrementally refine or transform the analysis information (fold or propagate analysis properties, etc.). However, I cannot think of a proper way to relate/map/bind the analysis dialect ops to the computation ops (SCEV expressions to LLVM IR, for the SE example). If analysis ops live in a separate function/region, I don’t see a clean way they can refer to the computation ops. Maybe we could do some tagging with attributes but that doesn’t sound very clean/robust to me. If analysis ops are intermixed with computation ops, they might interfere with other passes targeting computation ops.

Has anybody explored anything like this? Is this approach fundamentally wrong? :slight_smile:

Thanks,
Diego

I think one place where this idea would have significant applicability is when some aspect of the analysis (either the results or some intermediate data structure) is best represented as MLIR ops.

We have something like this in the shape computation world, where the reified shape calculations need to be associated with the values that they describe the shape of (which we can then either just infer shapes with, or maybe keep the shape calculations around and actually compute them).

There are 3 separate approaches that I’ve been able to come up with there:

the “identity-like” approach

%tensor = ...
%shape = ...
%tensor2 = tie_shape_identity(%tensor, %shape) // "identity-like" op that annotates the value.
// %tensor2 should be used instead of %tensor directly.

This approach has the advantage that is is very simple and allows very ad-hoc / unstructured annotations. The disadvantage is getting in the way of use-def chains.

IREE uses this approach when initially materializing shape calculations.

This approach is fraught with peril if one relies on having every %tensor annotated, as all sorts of random canonicalizations/patterns can break that invariant. Effectively, any transformation that introduces new Value’s into the program must be aware of this invariant and repair it, which is extremely hard. In IREE, it got so unmanageable that we only create this form immediately before the one pass that needs it, and erase the annotations immediately afterwards.

The “extra use” approach

%tensor = ...
%shape = ...
tie_shape_extra_use(%tensor, %shape)
// %tensor is used directly

This one is similar to the “identity-like” approach but interferes with use-def chains slightly differently: it introduces extra uses.

Similarly to the previous one, this approach is fraught with peril if one relies on having every %tensor annotated, as all sorts of random canonicalizations/patterns can break that invariant.

The “region” approach

%tensor_outer = tie_shape_region computation={
  %tensor = ...
  yield %tensor
} shape_calculation = {
  %shape = ...
  yield %shape
}
// %tensor_outer is used directly


OR

%shape = ...
%tensor_outer = tie_shape_region_capture %shape {
  %tensor = ...
  yield %tensor
}
// %tensor_outer is used directly

This approach has the advantage that sometimes there is some structure to the annotations that are needed. For example, you might have a fused subgraph, and only the shapes on the boundary of the subgraph matter. Optimizations can proceed inside the fused region with no interference due to the structure. Also, you can merge regions fairly easily in a domain-specific way.

This is similar to the tradeoffs that happen in IREE’s dispatch regions. (we start with identity-like and then convert to this form, essentially).

This approach has the disadvantage that it is bulky if there is no useful structure, and also if you have other structured region-based constructs in your IR that don’t strictly nest with the annotations, then you can’t use the approach at all.


This also ties in somewhat with the still unresolved general desire sometimes to have “attributes on Value’s” (instead of on Operations) which has a similar design space.

The biggest problem with analyses (no matter how you formulate or store their information) is invalidation of the analysis. At some point you have to make some sort of tradeoff between inhibiting a transformation so that the analysis information is not invalidated, leaving the analysis data in an invalid state that you need to somehow know to invalidate/ignore, or having transformations that understand and update it.

+1 to what Sean said has been discussed wrt Shape and shape computations. Using dialect ops to represent analysis information definitely possible and useful, but no good way to avoid them interfering with optimizations (now sometimes for the good, e.g., we can denote region where all invariants hold and so freely optimize within that region assuming the invariants without losing ability to verify invariants or resulting in indeterminate error reporting).

There is a 4th option - give things unique (wrt equivalence classes) identifiers and reference those identifiers in the analysis (not suggesting this as preferred option, just as an option :slight_smile: ). Given every op a “my_analysis_id” field and as refer to those in the seperate functions - at the cost that you’d need to go find an instruction with that ID somewhere in the module … Which sounds painful/expensive unless you keep a map in the analysis, reconstructing that map is O(N) post invalidation though [could probably do better by hooking into rewrite notifications for passes implemented via it].

Yes if you want to refer to SSA value inside a region, then you’d have to have something in that region, which can become quite painful and does count as a use which may impede optimizations. And you’d need to be able to identify when it would be invalidated.

As Sean mentioned we’ve discussed it a few times, but haven’t come up with a good solution quite yet. It feels like there is something there and the structure provided (at least wrt region based one, which I am fond of, I spell mine differently though and mostly related to witness/regions of invariants which just happen to also enable identifying the linkage simple :slight_smile: , but conceptually) is useful but can also obstruct/obscure.

It also depends on how long lived it would be and how many optimizations passes it should compose with and whether it pays for itself vs analysis structure. I’d love to see a prototype that pushes this to see where it breaks :slight_smile:

Some of these could also be new abstractions: just like affine map provides a very convenient and compact indexing representation, perhaps there could be a SCEV map & apply op so that it is a more restricted algebraic representation (akin to what we do with affine) that we know is useful for downstream users, but could also be expanded back to plain arithmetic during codegen. So it doesn’t represent analysis per-se but has executional semantics and is useful for analysis.

Thank you so much for sharing! Super useful!
I agree there is no silver bullet here. The ‘extra use’ and the ‘region’ approaches are the ones that I was thinking about. Totally agree with the advantages and disadvantages. It seems complicated esp. if the goal is to keep the analysis information around for a while and you cannot recompute it. My initial idea was to provide the analysis information at the MLIR entry point and then keep it around until it’s actually needed. However, that doesn’t sound like a good idea based on your experience.

This approach has the disadvantage that it is bulky if there is no useful structure, and also if you have other structured region-based constructs in your IR that don’t strictly nest with the annotations, then you can’t use the approach at all.

Yeah, even composability with other ‘region’ analyses would be problematic, if feasible at all.

Given every op a “my_analysis_id” field and as refer to those in the seperate functions - at the cost that you’d need to go find an instruction with that ID somewhere in the module … Which sounds painful/expensive unless you keep a map in the analysis, reconstructing that map is O(N) post invalidation though [could probably do better by hooking into rewrite notifications for passes implemented via it].

We discussed something similar internally some time ago. Having an analysis class to keep the mapping information would definitely help reduce the overhead and would also help keep track of whether the analysis is still valid or not. I need to better understand how the rewrite notifications could help here.

A 5th option that could work for cases where the analysis information is a constant property would be decorating the executional ops with analysis attributes. This is not using a dialect as a representation but it could have some (limited) applicability and it would be less invasive that some of the other approaches. Same disadvantages wrt invalidation, though.

Thanks! I’ll keep thinking about it.