Skip to content

Latest commit

 

History

History
166 lines (128 loc) · 7.91 KB

dynamic_shapes.md

File metadata and controls

166 lines (128 loc) · 7.91 KB

Dyanmic Shapes

NOTE: Effort is being made to make this facility generic so that it can be eventually upstreamed to MLIR in some fashion. However, because MLIR lacks a set of frontend ops and generally does not currently have any frontend oriented infrastructure, it is being prototyped within IREE in order to find a working set of ops and algorithms.

Levels of dynamicism

In general, there are three levels of shape information that can be present in the input IR (or trivially derived by applying some form of shape inferencing algorithm). Each additional one imposes more work on the compiler and runtime, so generally, the implementation progresses by addressing each once the former is well established:

  1. Fully static shapes: No tensors have dynamic dimensions. All tensors are ranked.
  2. Ranked Dynamicism: All tensors have ranks, but some dimensions may be unspecified.
  3. Unranked Dynamicism: Some tensors have indeterminate ranks.

At this stage, Dynamic Shapes in IREE refers to supporting dynamic ranked dynamic tensors, where some dimensions are left unspecified at public function boundaries. It is expected that once this is solid, some support can be considered for unranked dynamicism, and it is further expected that will entail new ops, algorithms and runtime support, apart from what is needed for ranked dynamicism.

Within the category of Ranked Dynamicism, it is well known that some dynamic dimensions are easier to deal with than others: in common DNN use, outer dimensions are much easier and more common with respect to code generation and kernel fanout than dynamic inner dimensions.

While the shape handling machinery is relatively generic, we expect that real backends will be limited with respect to how much they support all combinations of dynamic dimensions. Eventually, IREE intends to solve this by having relatively robust CPU fallback for fully dynamic cases and actionable warnings that pinpoint when more specificity could increase performance.

Compiler Frontend

In general, the IREE compiler frontend should accept modules containing functions with operands/results that have dynamic dimensions. Such functions may also have runtime dependent shapes in the form of GetShape-style ops which get a shape from an arbitrary tensor, perform some arithmetic on it and use the results elsewhere.

Shape dialect and lowering

IREE is introducing a shape dialect with a handful of ops and transformations that are useful for materializing dynamic shape computations in terms of high level ops on tensors.

Types:

  • ranked_shape: This value type represents the dynamic dimensions of a partially known, ranked shape. It is used early in the compiler to represent anywhere that dynamic dimensions need to be passed (i.e. function args/results, etc). At lower levels of the compiler, it will generally be dis-aggregated into loose SSA values. This type also carries the datatype used to represent the dimensions. This is currently fixed to i32 but may be leveraged eventually to use smaller integer when such things are known to be legal.

Ops:

  • get_ranked_shape: Takes a tensor SSA value and returns a corresponding ranked_shape. Early in the compilation flow, anything that needs a ranked shape should add such ops so that the compiler can later determine which shape computations to materialize. Getting the ranked_shape of a static tensor should yield a constant.
  • tie_shape: Takes tensor and ranked_shape SSA values and returns the tensor. This is used as a junction point by the shape materialization passes to know at various points precisely what the shape is.
  • ... TODO: need get_shape_dim and conversions to/from 1D tensors and loose SSA values.

Materialization

Function signature expansion

Early in the process, all functions should have their arguments and results expanded so that any dynamic tensors in their signature will gain a new argument/result for the corresponding ranked_shape. This is done by expanding the signatures and for arguments, inserting placeholder tie_shape ops which preserve the association for later materialization. For results, get_ranked_shape ops are inserted.

This is carried out by the iree-shape-expand-function-dynamic-dims pass, which uses the conversion framework under the hood to perform type expansion.

This pass is typically done early in the compiler flow.

Shape dependent codegen

A lot of scheduling logic will need to access shapes (i.e. allocation, workgroup size calculation, etc). In general, this should all be done based on a get_ranked_shape op and corresponding get_shape_dim ops. For fully static cases, these should reduce down to constants. For dynamic dimensions, the get_ranked_shape ops serve as anchors where later parts of the compiler know they need to materialize shape values.

Materializing shape computations

TODO: We have a sketch of this but are still proving it out.

Generally, it should be possible, for any get_ranked_shape op, to trace up the use-def chain and materialize shape manipulation arithmetic. Once materialized, a tie_shape op should be inserted to memorialize the junction. Eventually, every get_ranked_shape op should be follow a tie_shape op, and the canonicalization rules will elide the get_ranked_shape. There is complexity around blocks, control flow, etc, but this basic algorithm should be workable.

Work is ongoing upstream to provide a facility to register shape functions with ops, which would provide a dynamic, dialect independent way to know what arithmetic to materialize. However, in most cases this is not necessary. The built-in traits around types and sizes will allow most propagation to happen without shape functions. We intend to start with a static set of cases for the rest in order to prove the concept.

Scalarization

TODO: We have a sketch of this but are still proving it out.

It is quite common in real-world DNN usage to get the 1D tensor representing a shape and perform arbitrary tensor ops on it (usually basic arithmetic, slicing, concating, tiling, etc). While this is perfectly acceptable from a correctness standpoint, it is usually not performant: shapes are typically very small one dimensional vectors, and computations on them are usually trivial to reduce to small sequences of scalar machine code of a form that CPUs are very good at executing. Further, we often want to run these calculations eagerly when dispatching functions, etc (i.e. to pre-allocate buffers) and having them isolated (versus treating them as arbitrary dense computations) can be quite valuable.

We expect that the type bracketing that happens with ranked_shape and the corresponding ops will make it easy to write some simple DRR patterns to identify such shape manipulation sequences and lower them directly to regions of vm ops operating on scalars. Such regions can be retained and directly emitted when lowering to the vm dialect and/or CPU code generation and would run with low overhead along with any other scheduling code.

While an optimization, we suspect this is an important one.

Shape inference

TODO: This is mostly placeholder

There is work happening upstream to implement MLIR-integrated shape inference. In the mean-time, IREE expects that the input to the compiler has already had some shape inference performed on it. In practice, for TensorFlow, there is a pass which applies TensorFlow's pre-MLIR shape inference mechanisms to derive such things. This has limitations but is a reasonable starting point.

Compiler Backends

TODO: This is mostly placeholder.

Much of the newer structured-ops based codegen is capable of working (within bounds) with ranked dynamic shapes without much work. Given the lack of an e2e story, much of this has been done "by way of code review" and there are certainly issues to be resolved.

In addition, there are several ABI issues and negotiations with the backend that still need to be fleshed out.