forked from OSchip/llvm-project
1433 lines
59 KiB
Markdown
1433 lines
59 KiB
Markdown
# 'spv' Dialect
|
||
|
||
This document describes the design of the SPIR-V dialect in MLIR. It lists
|
||
various design choices we made for modeling different SPIR-V mechanisms, and
|
||
their rationale.
|
||
|
||
This document also explains in a high-level manner how different components are
|
||
organized and implemented in the code and gives steps to follow for extending
|
||
them.
|
||
|
||
This document assumes familiarity with SPIR-V. [SPIR-V][Spirv] is the Khronos
|
||
Group’s binary intermediate language for representing graphics shaders and
|
||
compute kernels. It is adopted by multiple Khronos Group’s APIs, including
|
||
Vulkan and OpenCL. It is fully defined in a
|
||
[human-readable specification][SpirvSpec]; the syntax of various SPIR-V
|
||
instructions are encoded in a [machine-readable grammar][SpirvGrammar].
|
||
|
||
[TOC]
|
||
|
||
## Design Guidelines
|
||
|
||
SPIR-V is a binary intermediate language that serves dual purpose: on one side,
|
||
it is an intermediate language to represent graphics shaders and compute kernels
|
||
for high-level languages to target; on the other side, it defines a stable
|
||
binary format for hardware driver consumption. As a result, SPIR-V has design
|
||
principles pertain to not only intermediate language, but also binary format.
|
||
For example, regularity is one of the design goals of SPIR-V. All concepts are
|
||
represented as SPIR-V instructions, including declaring extensions and
|
||
capabilities, defining types and constants, defining functions, attaching
|
||
additional properties to computation results, etc. This way favors binary
|
||
encoding and decoding for driver consumption but not necessarily compiler
|
||
transformations.
|
||
|
||
### Dialect design principles
|
||
|
||
The main objective of the SPIR-V dialect is to be a proper intermediate
|
||
representation (IR) to facilitate compiler transformations. While we still aim
|
||
to support serializing to and deserializing from the binary format for various
|
||
good reasons, the binary format and its concerns play less a role in the design
|
||
of the SPIR-V dialect: when there is a trade-off to be made between favoring IR
|
||
and supporting binary format, we lean towards the former.
|
||
|
||
On the IR aspect, the SPIR-V dialect aims to model SPIR-V at the same semantic
|
||
level. It is not intended to be a higher level or lower level abstraction than
|
||
the SPIR-V specification. Those abstractions are easily outside the domain of
|
||
SPIR-V and should be modeled with other proper dialects so they can be shared
|
||
among various compilation paths. Because of the dual purpose of SPIR-V, SPIR-V
|
||
dialect staying at the same semantic level as the SPIR-V specification also
|
||
means we can still have straightforward serialization and deserialization for
|
||
the majority of functionalities.
|
||
|
||
To summarize, the SPIR-V dialect follows the following design principles:
|
||
|
||
* Stay as the same semantic level as the SPIR-V specification by having
|
||
one-to-one mapping for most concepts and entities.
|
||
* Adopt SPIR-V specification's syntax if possible, but deviate intentionally
|
||
to utilize MLIR mechanisms if it results in better representation and
|
||
benefits transformation.
|
||
* Be straightforward to serialize into and deserialize from the SPIR-V binary
|
||
format.
|
||
|
||
SPIR-V is designed to be consumed by hardware drivers, so its representation is
|
||
quite clear, yet verbose for some cases. Allowing representational deviation
|
||
gives us the flexibility to reduce the verbosity by using MLIR mechanisms.
|
||
|
||
### Dialect scopes
|
||
|
||
SPIR-V supports multiple execution environments, specified by client APIs.
|
||
Notable adopters include Vulkan and OpenCL. It follows that the SPIR-V dialect
|
||
should support multiple execution environments if to be a proper proxy of SPIR-V
|
||
in MLIR systems. The SPIR-V dialect is designed with these considerations: it
|
||
has proper support for versions, extensions, and capabilities and is as
|
||
extensible as SPIR-V specification.
|
||
|
||
## Conventions
|
||
|
||
The SPIR-V dialect adopts the following conventions for IR:
|
||
|
||
* The prefix for all SPIR-V types and operations are `spv.`.
|
||
* All instructions in an extended instruction set are further qualified with
|
||
the extended instruction set's prefix. For example, all operations in the
|
||
GLSL extended instruction set have the prefix of `spv.GLSL.`.
|
||
* Ops that directly mirror instructions in the specification have `CamelCase`
|
||
names that are the same as the instruction opnames (without the `Op`
|
||
prefix). For example, `spv.FMul` is a direct mirror of `OpFMul` in the
|
||
specification. Such an op will be serialized into and deserialized from one
|
||
SPIR-V instruction.
|
||
* Ops with `snake_case` names are those that have different representation
|
||
from corresponding instructions (or concepts) in the specification. These
|
||
ops are mostly for defining the SPIR-V structure. For example, `spv.module`
|
||
and `spv.Constant`. They may correspond to one or more instructions during
|
||
(de)serialization.
|
||
* Ops with `mlir.snake_case` names are those that have no corresponding
|
||
instructions (or concepts) in the binary format. They are introduced to
|
||
satisfy MLIR structural requirements. For example, `spv.mlir.merge`. They
|
||
map to no instructions during (de)serialization.
|
||
|
||
(TODO: consider merging the last two cases and adopting `spv.mlir.` prefix for
|
||
them.)
|
||
|
||
## Module
|
||
|
||
A SPIR-V module is defined via the `spv.module` op, which has one region that
|
||
contains one block. Model-level instructions, including function definitions,
|
||
are all placed inside the block. Functions are defined using the builtin `func`
|
||
op.
|
||
|
||
We choose to model a SPIR-V module with a dedicated `spv.module` op based on the
|
||
following considerations:
|
||
|
||
* It maps cleanly to a SPIR-V module in the specification.
|
||
* We can enforce SPIR-V specific verification that is suitable to be performed
|
||
at the module-level.
|
||
* We can attach additional model-level attributes.
|
||
* We can control custom assembly form.
|
||
|
||
The `spv.module` op's region cannot capture SSA values from outside, neither
|
||
implicitly nor explicitly. The `spv.module` op's region is closed as to what ops
|
||
can appear inside: apart from the builtin `func` op, it can only contain ops
|
||
from the SPIR-V dialect. The `spv.module` op's verifier enforces this rule. This
|
||
meaningfully guarantees that a `spv.module` can be the entry point and boundary
|
||
for serialization.
|
||
|
||
### Module-level operations
|
||
|
||
SPIR-V binary format defines the following [sections][SpirvLogicalLayout]:
|
||
|
||
1. Capabilities required by the module.
|
||
1. Extensions required by the module.
|
||
1. Extended instructions sets required by the module.
|
||
1. Addressing and memory model specification.
|
||
1. Entry point specifications.
|
||
1. Execution mode declarations.
|
||
1. Debug instructions.
|
||
1. Annotation/decoration instructions.
|
||
1. Type, constant, global variables.
|
||
1. Function declarations.
|
||
1. Function definitions.
|
||
|
||
Basically, a SPIR-V binary module contains multiple module-level instructions
|
||
followed by a list of functions. Those module-level instructions are essential
|
||
and they can generate result ids referenced by functions, notably, declaring
|
||
resource variables to interact with the execution environment.
|
||
|
||
Compared to the binary format, we adjust how these module-level SPIR-V
|
||
instructions are represented in the SPIR-V dialect:
|
||
|
||
#### Use MLIR attributes for metadata
|
||
|
||
* Requirements for capabilities, extensions, extended instruction sets,
|
||
addressing model, and memory model are conveyed using `spv.module`
|
||
attributes. This is considered better because these information are for the
|
||
execution environment. It's easier to probe them if on the module op itself.
|
||
* Annotations/decoration instructions are "folded" into the instructions they
|
||
decorate and represented as attributes on those ops. This eliminates
|
||
potential forward references of SSA values, improves IR readability, and
|
||
makes querying the annotations more direct. More discussions can be found in
|
||
the [`Decorations`](#decorations) section.
|
||
|
||
#### Model types with MLIR custom types
|
||
|
||
* Types are represented using MLIR builtin types and SPIR-V dialect specific
|
||
types. There are no type declaration ops in the SPIR-V dialect. More
|
||
discussions can be found in the [Types](#types) section later.
|
||
|
||
#### Unify and localize constants
|
||
|
||
* Various normal constant instructions are represented by the same
|
||
`spv.Constant` op. Those instructions are just for constants of different
|
||
types; using one op to represent them reduces IR verbosity and makes
|
||
transformations less tedious.
|
||
* Normal constants are not placed in `spv.module`'s region; they are localized
|
||
into functions. This is to make functions in the SPIR-V dialect to be
|
||
isolated and explicit capturing. Constants are cheap to duplicate given
|
||
attributes are made unique in `MLIRContext`.
|
||
|
||
#### Adopt symbol-based global variables and specialization constant
|
||
|
||
* Global variables are defined with the `spv.GlobalVariable` op. They do not
|
||
generate SSA values. Instead they have symbols and should be referenced via
|
||
symbols. To use global variables in a function block, `spv.mlir.addressof` is
|
||
needed to turn the symbol into an SSA value.
|
||
* Specialization constants are defined with the `spv.SpecConstant` op. Similar
|
||
to global variables, they do not generate SSA values and have symbols for
|
||
reference, too. `spv.mlir.referenceof` is needed to turn the symbol into an SSA
|
||
value for use in a function block.
|
||
|
||
The above choices enables functions in the SPIR-V dialect to be isolated and
|
||
explicit capturing.
|
||
|
||
#### Disallow implicit capturing in functions
|
||
|
||
* In SPIR-V specification, functions support implicit capturing: they can
|
||
reference SSA values defined in modules. In the SPIR-V dialect functions are
|
||
defined with `func` op, which disallows implicit capturing. This is more
|
||
friendly to compiler analyses and transformations. More discussions can be
|
||
found in the [Function](#function) section later.
|
||
|
||
#### Model entry points and execution models as normal ops
|
||
|
||
* A SPIR-V module can have multiple entry points. And these entry points refer
|
||
to the function and interface variables. It’s not suitable to model them as
|
||
`spv.module` op attributes. We can model them as normal ops of using symbol
|
||
references.
|
||
* Similarly for execution modes, which are coupled with entry points, we can
|
||
model them as normal ops in `spv.module`'s region.
|
||
|
||
## Decorations
|
||
|
||
Annotations/decorations provide additional information on result ids. In SPIR-V,
|
||
all instructions can generate result ids, including value-computing and
|
||
type-defining ones.
|
||
|
||
For decorations on value result ids, we can just have a corresponding attribute
|
||
attached to the operation generating the SSA value. For example, for the
|
||
following SPIR-V:
|
||
|
||
```spirv
|
||
OpDecorate %v1 RelaxedPrecision
|
||
OpDecorate %v2 NoContraction
|
||
...
|
||
%v1 = OpFMul %float %0 %0
|
||
%v2 = OpFMul %float %1 %1
|
||
```
|
||
|
||
We can represent them in the SPIR-V dialect as:
|
||
|
||
```mlir
|
||
%v1 = "spv.FMul"(%0, %0) {RelaxedPrecision: unit} : (f32, f32) -> (f32)
|
||
%v2 = "spv.FMul"(%1, %1) {NoContraction: unit} : (f32, f32) -> (f32)
|
||
```
|
||
|
||
This approach benefits transformations. Essentially those decorations are just
|
||
additional properties of the result ids (and thus their defining instructions).
|
||
In SPIR-V binary format, they are just represented as instructions. Literally
|
||
following SPIR-V binary format means we need to through def-use chains to find
|
||
the decoration instructions and query information from them.
|
||
|
||
For decorations on type result ids, notice that practically, only result ids
|
||
generated from composite types (e.g., `OpTypeArray`, `OpTypeStruct`) need to be
|
||
decorated for memory layouting purpose (e.g., `ArrayStride`, `Offset`, etc.);
|
||
scalar/vector types are required to be uniqued in SPIR-V. Therefore, we can just
|
||
encode them directly in the dialect-specific type.
|
||
|
||
## Types
|
||
|
||
Theoretically we can define all SPIR-V types using MLIR extensible type system,
|
||
but other than representational purity, it does not buy us more. Instead, we
|
||
need to maintain the code and invest in pretty printing them. So we prefer to
|
||
use builtin types if possible.
|
||
|
||
The SPIR-V dialect reuses builtin integer, float, and vector types:
|
||
|
||
Specification | Dialect
|
||
:----------------------------------: | :-------------------------------:
|
||
`OpTypeBool` | `i1`
|
||
`OpTypeFloat <bitwidth>` | `f<bitwidth>`
|
||
`OpTypeVector <scalar-type> <count>` | `vector<<count> x <scalar-type>>`
|
||
|
||
For integer types, the SPIR-V dialect supports all signedness semantics
|
||
(signless, signed, unsigned) in order to ease transformations from higher level
|
||
dialects. However, SPIR-V spec only defines two signedness semantics state: 0
|
||
indicates unsigned, or no signedness semantics, 1 indicates signed semantics. So
|
||
both `iN` and `uiN` are serialized into the same `OpTypeInt N 0`. For
|
||
deserialization, we always treat `OpTypeInt N 0` as `iN`.
|
||
|
||
`mlir::NoneType` is used for SPIR-V `OpTypeVoid`; builtin function types are
|
||
used for SPIR-V `OpTypeFunction` types.
|
||
|
||
The SPIR-V dialect and defines the following dialect-specific types:
|
||
|
||
```
|
||
spirv-type ::= array-type
|
||
| image-type
|
||
| pointer-type
|
||
| runtime-array-type
|
||
| sampled-image-type
|
||
| struct-type
|
||
```
|
||
|
||
### Array type
|
||
|
||
This corresponds to SPIR-V [array type][ArrayType]. Its syntax is
|
||
|
||
```
|
||
element-type ::= integer-type
|
||
| floating-point-type
|
||
| vector-type
|
||
| spirv-type
|
||
|
||
array-type ::= `!spv.array` `<` integer-literal `x` element-type
|
||
(`,` `stride` `=` integer-literal)? `>`
|
||
```
|
||
|
||
For example,
|
||
|
||
```mlir
|
||
!spv.array<4 x i32>
|
||
!spv.array<4 x i32, stride = 4>
|
||
!spv.array<16 x vector<4 x f32>>
|
||
```
|
||
|
||
### Image type
|
||
|
||
This corresponds to SPIR-V [image type][ImageType]. Its syntax is
|
||
|
||
```
|
||
dim ::= `1D` | `2D` | `3D` | `Cube` | <and other SPIR-V Dim specifiers...>
|
||
|
||
depth-info ::= `NoDepth` | `IsDepth` | `DepthUnknown`
|
||
|
||
arrayed-info ::= `NonArrayed` | `Arrayed`
|
||
|
||
sampling-info ::= `SingleSampled` | `MultiSampled`
|
||
|
||
sampler-use-info ::= `SamplerUnknown` | `NeedSampler` | `NoSampler`
|
||
|
||
format ::= `Unknown` | `Rgba32f` | <and other SPIR-V Image Formats...>
|
||
|
||
image-type ::= `!spv.image<` element-type `,` dim `,` depth-info `,`
|
||
arrayed-info `,` sampling-info `,`
|
||
sampler-use-info `,` format `>`
|
||
```
|
||
|
||
For example,
|
||
|
||
```mlir
|
||
!spv.image<f32, 1D, NoDepth, NonArrayed, SingleSampled, SamplerUnknown, Unknown>
|
||
!spv.image<f32, Cube, IsDepth, Arrayed, MultiSampled, NeedSampler, Rgba32f>
|
||
```
|
||
|
||
### Pointer type
|
||
|
||
This corresponds to SPIR-V [pointer type][PointerType]. Its syntax is
|
||
|
||
```
|
||
storage-class ::= `UniformConstant`
|
||
| `Uniform`
|
||
| `Workgroup`
|
||
| <and other storage classes...>
|
||
|
||
pointer-type ::= `!spv.ptr<` element-type `,` storage-class `>`
|
||
```
|
||
|
||
For example,
|
||
|
||
```mlir
|
||
!spv.ptr<i32, Function>
|
||
!spv.ptr<vector<4 x f32>, Uniform>
|
||
```
|
||
|
||
### Runtime array type
|
||
|
||
This corresponds to SPIR-V [runtime array type][RuntimeArrayType]. Its syntax is
|
||
|
||
```
|
||
runtime-array-type ::= `!spv.rtarray` `<` element-type (`,` `stride` `=` integer-literal)? `>`
|
||
```
|
||
|
||
For example,
|
||
|
||
```mlir
|
||
!spv.rtarray<i32>
|
||
!spv.rtarray<i32, stride=4>
|
||
!spv.rtarray<vector<4 x f32>>
|
||
```
|
||
### Sampled image type
|
||
|
||
This corresponds to SPIR-V [sampled image type][SampledImageType]. Its syntax is
|
||
|
||
```
|
||
sampled-image-type ::= `!spv.sampled_image<!spv.image<` element-type `,` dim `,` depth-info `,`
|
||
arrayed-info `,` sampling-info `,`
|
||
sampler-use-info `,` format `>>`
|
||
```
|
||
|
||
For example,
|
||
|
||
```mlir
|
||
!spv.sampled_image<!spv.image<f32, Dim1D, NoDepth, NonArrayed, SingleSampled, NoSampler, Unknown>>
|
||
!spv.sampled_image<!spv.image<i32, Rect, DepthUnknown, Arrayed, MultiSampled, NeedSampler, R8ui>>
|
||
```
|
||
|
||
### Struct type
|
||
|
||
This corresponds to SPIR-V [struct type][StructType]. Its syntax is
|
||
|
||
```
|
||
struct-member-decoration ::= integer-literal? spirv-decoration*
|
||
struct-type ::= `!spv.struct<` spirv-type (`[` struct-member-decoration `]`)?
|
||
(`, ` spirv-type (`[` struct-member-decoration `]`)?
|
||
```
|
||
|
||
For Example,
|
||
|
||
```mlir
|
||
!spv.struct<f32>
|
||
!spv.struct<f32 [0]>
|
||
!spv.struct<f32, !spv.image<f32, 1D, NoDepth, NonArrayed, SingleSampled, SamplerUnknown, Unknown>>
|
||
!spv.struct<f32 [0], i32 [4]>
|
||
```
|
||
|
||
## Function
|
||
|
||
In SPIR-V, a function construct consists of multiple instructions involving
|
||
`OpFunction`, `OpFunctionParameter`, `OpLabel`, `OpFunctionEnd`.
|
||
|
||
```spirv
|
||
// int f(int v) { return v; }
|
||
%1 = OpTypeInt 32 0
|
||
%2 = OpTypeFunction %1 %1
|
||
%3 = OpFunction %1 %2
|
||
%4 = OpFunctionParameter %1
|
||
%5 = OpLabel
|
||
%6 = OpReturnValue %4
|
||
OpFunctionEnd
|
||
```
|
||
|
||
This construct is very clear yet quite verbose. It is intended for driver
|
||
consumption. There is little benefit to literally replicate this construct in
|
||
the SPIR-V dialect. Instead, we reuse the builtin `func` op to express functions
|
||
more concisely:
|
||
|
||
```mlir
|
||
func @f(%arg: i32) -> i32 {
|
||
"spv.ReturnValue"(%arg) : (i32) -> (i32)
|
||
}
|
||
```
|
||
|
||
A SPIR-V function can have at most one result. It cannot contain nested
|
||
functions or non-SPIR-V operations. `spv.module` verifies these requirements.
|
||
|
||
A major difference between the SPIR-V dialect and the SPIR-V specification for
|
||
functions is that the former are isolated and require explicit capturing, while
|
||
the latter allows implicit capturing. In SPIR-V specification, functions can
|
||
refer to SSA values (generated by constants, global variables, etc.) defined in
|
||
modules. The SPIR-V dialect adjusted how constants and global variables are
|
||
modeled to enable isolated functions. Isolated functions are more friendly to
|
||
compiler analyses and transformations. This also enables the SPIR-V dialect to
|
||
better utilize core infrastructure: many functionalities in the core
|
||
infrastructure require ops to be isolated, e.g., the
|
||
[greedy pattern rewriter][GreedyPatternRewriter] can only act on ops isolated
|
||
from above.
|
||
|
||
(TODO: create a dedicated `spv.fn` op for SPIR-V functions.)
|
||
|
||
## Operations
|
||
|
||
In SPIR-V, instruction is a generalized concept; a SPIR-V module is just a
|
||
sequence of instructions. Declaring types, expressing computations, annotating
|
||
result ids, expressing control flows and others are all in the form of
|
||
instructions.
|
||
|
||
We only discuss instructions expressing computations here, which can be
|
||
represented via SPIR-V dialect ops. Module-level instructions for declarations
|
||
and definitions are represented differently in the SPIR-V dialect as explained
|
||
earlier in the [Module-level operations](#module-level-operations) section.
|
||
|
||
An instruction computes zero or one result from zero or more operands. The
|
||
result is a new result id. An operand can be a result id generated by a previous
|
||
instruction, an immediate value, or a case of an enum type. We can model result
|
||
id operands and results with MLIR SSA values; for immediate value and enum
|
||
cases, we can model them with MLIR attributes.
|
||
|
||
For example,
|
||
|
||
```spirv
|
||
%i32 = OpTypeInt 32 0
|
||
%c42 = OpConstant %i32 42
|
||
...
|
||
%3 = OpVariable %i32 Function 42
|
||
%4 = OpIAdd %i32 %c42 %c42
|
||
```
|
||
|
||
can be represented in the dialect as
|
||
|
||
```mlir
|
||
%0 = "spv.Constant"() { value = 42 : i32 } : () -> i32
|
||
%1 = "spv.Variable"(%0) { storage_class = "Function" } : (i32) -> !spv.ptr<i32, Function>
|
||
%2 = "spv.IAdd"(%0, %0) : (i32, i32) -> i32
|
||
```
|
||
|
||
Operation documentation is written in each op's Op Definition Spec using
|
||
TableGen. A markdown version of the doc can be generated using
|
||
`mlir-tblgen -gen-doc` and is attached in the
|
||
[Operation definitions](#operation-definitions) section.
|
||
|
||
### Ops from extended instruction sets
|
||
|
||
Analogically extended instruction set is a mechanism to import SPIR-V
|
||
instructions within another namespace. [`GLSL.std.450`][GlslStd450] is an
|
||
extended instruction set that provides common mathematical routines that should
|
||
be supported. Instead of modeling `OpExtInstImport` as a separate op and use a
|
||
single op to model `OpExtInst` for all extended instructions, we model each
|
||
SPIR-V instruction in an extended instruction set as a separate op with the
|
||
proper name prefix. For example, for
|
||
|
||
```spirv
|
||
%glsl = OpExtInstImport "GLSL.std.450"
|
||
|
||
%f32 = OpTypeFloat 32
|
||
%cst = OpConstant %f32 ...
|
||
|
||
%1 = OpExtInst %f32 %glsl 28 %cst
|
||
%2 = OpExtInst %f32 %glsl 31 %cst
|
||
```
|
||
|
||
we can have
|
||
|
||
```mlir
|
||
%1 = "spv.GLSL.Log"(%cst) : (f32) -> (f32)
|
||
%2 = "spv.GLSL.Sqrt"(%cst) : (f32) -> (f32)
|
||
```
|
||
|
||
## Control Flow
|
||
|
||
SPIR-V binary format uses merge instructions (`OpSelectionMerge` and
|
||
`OpLoopMerge`) to declare structured control flow. They explicitly declare a
|
||
header block before the control flow diverges and a merge block where control
|
||
flow subsequently converges. These blocks delimit constructs that must nest, and
|
||
can only be entered and exited in structured ways.
|
||
|
||
In the SPIR-V dialect, we use regions to mark the boundary of a structured
|
||
control flow construct. With this approach, it's easier to discover all blocks
|
||
belonging to a structured control flow construct. It is also more idiomatic to
|
||
MLIR system.
|
||
|
||
We introduce a `spv.mlir.selection` and `spv.mlir.loop` op for structured selections and
|
||
loops, respectively. The merge targets are the next ops following them. Inside
|
||
their regions, a special terminator, `spv.mlir.merge` is introduced for branching to
|
||
the merge target.
|
||
|
||
### Selection
|
||
|
||
`spv.mlir.selection` defines a selection construct. It contains one region. The
|
||
region should contain at least two blocks: one selection header block and one
|
||
merge block.
|
||
|
||
* The selection header block should be the first block. It should contain the
|
||
`spv.BranchConditional` or `spv.Switch` op.
|
||
* The merge block should be the last block. The merge block should only
|
||
contain a `spv.mlir.merge` op. Any block can branch to the merge block for early
|
||
exit.
|
||
|
||
```
|
||
+--------------+
|
||
| header block | (may have multiple outgoing branches)
|
||
+--------------+
|
||
/ | \
|
||
...
|
||
|
||
|
||
+---------+ +---------+ +---------+
|
||
| case #0 | | case #1 | | case #2 | ... (may have branches between each other)
|
||
+---------+ +---------+ +---------+
|
||
|
||
|
||
...
|
||
\ | /
|
||
v
|
||
+-------------+
|
||
| merge block | (may have multiple incoming branches)
|
||
+-------------+
|
||
```
|
||
|
||
For example, for the given function
|
||
|
||
```c++
|
||
void loop(bool cond) {
|
||
int x = 0;
|
||
if (cond) {
|
||
x = 1;
|
||
} else {
|
||
x = 2;
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
It will be represented as
|
||
|
||
```mlir
|
||
func @selection(%cond: i1) -> () {
|
||
%zero = spv.Constant 0: i32
|
||
%one = spv.Constant 1: i32
|
||
%two = spv.Constant 2: i32
|
||
%x = spv.Variable init(%zero) : !spv.ptr<i32, Function>
|
||
|
||
spv.mlir.selection {
|
||
spv.BranchConditional %cond, ^then, ^else
|
||
|
||
^then:
|
||
spv.Store "Function" %x, %one : i32
|
||
spv.Branch ^merge
|
||
|
||
^else:
|
||
spv.Store "Function" %x, %two : i32
|
||
spv.Branch ^merge
|
||
|
||
^merge:
|
||
spv.mlir.merge
|
||
}
|
||
|
||
// ...
|
||
}
|
||
|
||
```
|
||
|
||
### Loop
|
||
|
||
`spv.mlir.loop` defines a loop construct. It contains one region. The region should
|
||
contain at least four blocks: one entry block, one loop header block, one loop
|
||
continue block, one merge block.
|
||
|
||
* The entry block should be the first block and it should jump to the loop
|
||
header block, which is the second block.
|
||
* The merge block should be the last block. The merge block should only
|
||
contain a `spv.mlir.merge` op. Any block except the entry block can branch to
|
||
the merge block for early exit.
|
||
* The continue block should be the second to last block and it should have a
|
||
branch to the loop header block.
|
||
* The loop continue block should be the only block, except the entry block,
|
||
branching to the loop header block.
|
||
|
||
```
|
||
+-------------+
|
||
| entry block | (one outgoing branch)
|
||
+-------------+
|
||
|
|
||
v
|
||
+-------------+ (two incoming branches)
|
||
| loop header | <-----+ (may have one or two outgoing branches)
|
||
+-------------+ |
|
||
|
|
||
... |
|
||
\ | / |
|
||
v |
|
||
+---------------+ | (may have multiple incoming branches)
|
||
| loop continue | -----+ (may have one or two outgoing branches)
|
||
+---------------+
|
||
|
||
...
|
||
\ | /
|
||
v
|
||
+-------------+ (may have multiple incoming branches)
|
||
| merge block |
|
||
+-------------+
|
||
```
|
||
|
||
The reason to have another entry block instead of directly using the loop header
|
||
block as the entry block is to satisfy region's requirement: entry block of
|
||
region may not have predecessors. We have a merge block so that branch ops can
|
||
reference it as successors. The loop continue block here corresponds to
|
||
"continue construct" using SPIR-V spec's term; it does not mean the "continue
|
||
block" as defined in the SPIR-V spec, which is "a block containing a branch to
|
||
an OpLoopMerge instruction’s Continue Target."
|
||
|
||
For example, for the given function
|
||
|
||
```c++
|
||
void loop(int count) {
|
||
for (int i = 0; i < count; ++i) {
|
||
// ...
|
||
}
|
||
}
|
||
```
|
||
|
||
It will be represented as
|
||
|
||
```mlir
|
||
func @loop(%count : i32) -> () {
|
||
%zero = spv.Constant 0: i32
|
||
%one = spv.Constant 1: i32
|
||
%var = spv.Variable init(%zero) : !spv.ptr<i32, Function>
|
||
|
||
spv.mlir.loop {
|
||
spv.Branch ^header
|
||
|
||
^header:
|
||
%val0 = spv.Load "Function" %var : i32
|
||
%cmp = spv.SLessThan %val0, %count : i32
|
||
spv.BranchConditional %cmp, ^body, ^merge
|
||
|
||
^body:
|
||
// ...
|
||
spv.Branch ^continue
|
||
|
||
^continue:
|
||
%val1 = spv.Load "Function" %var : i32
|
||
%add = spv.IAdd %val1, %one : i32
|
||
spv.Store "Function" %var, %add : i32
|
||
spv.Branch ^header
|
||
|
||
^merge:
|
||
spv.mlir.merge
|
||
}
|
||
return
|
||
}
|
||
```
|
||
|
||
### Block argument for Phi
|
||
|
||
There are no direct Phi operations in the SPIR-V dialect; SPIR-V `OpPhi`
|
||
instructions are modelled as block arguments in the SPIR-V dialect. (See the
|
||
[Rationale][Rationale] doc for "Block Arguments vs Phi nodes".) Each block
|
||
argument corresponds to one `OpPhi` instruction in the SPIR-V binary format. For
|
||
example, for the following SPIR-V function `foo`:
|
||
|
||
```spirv
|
||
%foo = OpFunction %void None ...
|
||
%entry = OpLabel
|
||
%var = OpVariable %_ptr_Function_int Function
|
||
OpSelectionMerge %merge None
|
||
OpBranchConditional %true %true %false
|
||
%true = OpLabel
|
||
OpBranch %phi
|
||
%false = OpLabel
|
||
OpBranch %phi
|
||
%phi = OpLabel
|
||
%val = OpPhi %int %int_1 %false %int_0 %true
|
||
OpStore %var %val
|
||
OpReturn
|
||
%merge = OpLabel
|
||
OpReturn
|
||
OpFunctionEnd
|
||
```
|
||
|
||
It will be represented as:
|
||
|
||
```mlir
|
||
func @foo() -> () {
|
||
%var = spv.Variable : !spv.ptr<i32, Function>
|
||
|
||
spv.mlir.selection {
|
||
%true = spv.Constant true
|
||
spv.BranchConditional %true, ^true, ^false
|
||
|
||
^true:
|
||
%zero = spv.Constant 0 : i32
|
||
spv.Branch ^phi(%zero: i32)
|
||
|
||
^false:
|
||
%one = spv.Constant 1 : i32
|
||
spv.Branch ^phi(%one: i32)
|
||
|
||
^phi(%arg: i32):
|
||
spv.Store "Function" %var, %arg : i32
|
||
spv.Return
|
||
|
||
^merge:
|
||
spv.mlir.merge
|
||
}
|
||
spv.Return
|
||
}
|
||
```
|
||
|
||
## Version, extensions, capabilities
|
||
|
||
SPIR-V supports versions, extensions, and capabilities as ways to indicate the
|
||
availability of various features (types, ops, enum cases) on target hardware.
|
||
For example, non-uniform group operations were missing before v1.3, and they
|
||
require special capabilities like `GroupNonUniformArithmetic` to be used. These
|
||
availability information relates to [target environment](#target-environment)
|
||
and affects the legality of patterns during dialect conversion.
|
||
|
||
SPIR-V ops' availability requirements are modeled with
|
||
[op interfaces][MlirOpInterface]:
|
||
|
||
* `QueryMinVersionInterface` and `QueryMaxVersionInterface` for version
|
||
requirements
|
||
* `QueryExtensionInterface` for extension requirements
|
||
* `QueryCapabilityInterface` for capability requirements
|
||
|
||
These interface declarations are auto-generated from TableGen definitions
|
||
included in [`SPIRVBase.td`][MlirSpirvBase]. At the moment all SPIR-V ops
|
||
implement the above interfaces.
|
||
|
||
SPIR-V ops' availability implementation methods are automatically synthesized
|
||
from the availability specification on each op and enum attribute in TableGen.
|
||
An op needs to look into not only the opcode but also operands to derive its
|
||
availability requirements. For example, `spv.ControlBarrier` requires no
|
||
special capability if the execution scope is `Subgroup`, but it will require
|
||
the `VulkanMemoryModel` capability if the scope is `QueueFamily`.
|
||
|
||
SPIR-V types' availability implementation methods are manually written as
|
||
overrides in the SPIR-V [type hierarchy][MlirSpirvTypes].
|
||
|
||
These availability requirements serve as the "ingredients" for the
|
||
[`SPIRVConversionTarget`](#spirvconversiontarget) and
|
||
[`SPIRVTypeConverter`](#spirvtypeconverter) to perform op and type conversions,
|
||
by following the requirements in [target environment](#target-environment).
|
||
|
||
## Target environment
|
||
|
||
SPIR-V aims to support multiple execution environments as specified by client
|
||
APIs. These execution environments affect the availability of certain SPIR-V
|
||
features. For example, a [Vulkan 1.1][VulkanSpirv] implementation must support
|
||
the 1.0, 1.1, 1.2, and 1.3 versions of SPIR-V and the 1.0 version of the SPIR-V
|
||
extended instructions for GLSL. Further Vulkan extensions may enable more SPIR-V
|
||
instructions.
|
||
|
||
SPIR-V compilation should also take into consideration of the execution
|
||
environment, so we generate SPIR-V modules valid for the target environment.
|
||
This is conveyed by the `spv.target_env` (`spirv::TargetEnvAttr`) attribute. It
|
||
should be of `#spv.target_env` attribute kind, which is defined as:
|
||
|
||
```
|
||
spirv-version ::= `v1.0` | `v1.1` | ...
|
||
spirv-extension ::= `SPV_KHR_16bit_storage` | `SPV_EXT_physical_storage_buffer` | ...
|
||
spirv-capability ::= `Shader` | `Kernel` | `GroupNonUniform` | ...
|
||
|
||
spirv-extension-list ::= `[` (spirv-extension-elements)? `]`
|
||
spirv-extension-elements ::= spirv-extension (`,` spirv-extension)*
|
||
|
||
spirv-capability-list ::= `[` (spirv-capability-elements)? `]`
|
||
spirv-capability-elements ::= spirv-capability (`,` spirv-capability)*
|
||
|
||
spirv-resource-limits ::= dictionary-attribute
|
||
|
||
spirv-vce-attribute ::= `#` `spv.vce` `<`
|
||
spirv-version `,`
|
||
spirv-capability-list `,`
|
||
spirv-extensions-list `>`
|
||
|
||
spirv-vendor-id ::= `AMD` | `NVIDIA` | ...
|
||
spirv-device-type ::= `DiscreteGPU` | `IntegratedGPU` | `CPU` | ...
|
||
spirv-device-id ::= integer-literal
|
||
spirv-device-info ::= spirv-vendor-id (`:` spirv-device-type (`:` spirv-device-id)?)?
|
||
|
||
spirv-target-env-attribute ::= `#` `spv.target_env` `<`
|
||
spirv-vce-attribute,
|
||
(spirv-device-info `,`)?
|
||
spirv-resource-limits `>`
|
||
```
|
||
|
||
The attribute has a few fields:
|
||
|
||
* A `#spv.vce` (`spirv::VerCapExtAttr`) attribute:
|
||
* The target SPIR-V version.
|
||
* A list of SPIR-V extensions for the target.
|
||
* A list of SPIR-V capabilities for the target.
|
||
* A dictionary of target resource limits (see the
|
||
[Vulkan spec][VulkanResourceLimits] for explanation):
|
||
* `max_compute_workgroup_invocations`
|
||
* `max_compute_workgroup_size`
|
||
|
||
For example,
|
||
|
||
```
|
||
module attributes {
|
||
spv.target_env = #spv.target_env<
|
||
#spv.vce<v1.3, [Shader, GroupNonUniform], [SPV_KHR_8bit_storage]>,
|
||
ARM:IntegratedGPU,
|
||
{
|
||
max_compute_workgroup_invocations = 128 : i32,
|
||
max_compute_workgroup_size = dense<[128, 128, 64]> : vector<3xi32>
|
||
}>
|
||
} { ... }
|
||
```
|
||
|
||
Dialect conversion framework will utilize the information in `spv.target_env` to
|
||
properly filter out patterns and ops not available in the target execution
|
||
environment. When targeting SPIR-V, one needs to create a
|
||
[`SPIRVConversionTarget`](#spirvconversiontarget) by providing such an
|
||
attribute.
|
||
|
||
## Shader interface (ABI)
|
||
|
||
SPIR-V itself is just expressing computation happening on GPU device. SPIR-V
|
||
programs themselves are not enough for running workloads on GPU; a companion
|
||
host application is needed to manage the resources referenced by SPIR-V programs
|
||
and dispatch the workload. For the Vulkan execution environment, the host
|
||
application will be written using Vulkan API. Unlike CUDA, the SPIR-V program
|
||
and the Vulkan application are typically authored with different front-end
|
||
languages, which isolates these two worlds. Yet they still need to match
|
||
_interfaces_: the variables declared in a SPIR-V program for referencing
|
||
resources need to match with the actual resources managed by the application
|
||
regarding their parameters.
|
||
|
||
Still using Vulkan as an example execution environment, there are two primary
|
||
resource types in Vulkan: buffers and images. They are used to back various uses
|
||
that may differ regarding the classes of operations (load, store, atomic) to be
|
||
performed. These uses are differentiated via descriptor types. (For example,
|
||
uniform storage buffer descriptors can only support load operations while
|
||
storage buffer descriptors can support load, store, and atomic operations.)
|
||
Vulkan uses a binding model for resources. Resources are associated with
|
||
descriptors and descriptors are further grouped into sets. Each descriptor thus
|
||
has a set number and a binding number. Descriptors in the application
|
||
corresponds to variables in the SPIR-V program. Their parameters must match,
|
||
including but not limited to set and binding numbers.
|
||
|
||
Apart from buffers and images, there is other data that is set up by Vulkan and
|
||
referenced inside the SPIR-V program, for example, push constants. They also
|
||
have parameters that require matching between the two worlds.
|
||
|
||
The interface requirements are external information to the SPIR-V compilation
|
||
path in MLIR. Besides, each Vulkan application may want to handle resources
|
||
differently. To avoid duplication and to share common utilities, a SPIR-V shader
|
||
interface specification needs to be defined to provide the external requirements
|
||
to and guide the SPIR-V compilation path.
|
||
|
||
### Shader interface attributes
|
||
|
||
The SPIR-V dialect defines [a few attributes][MlirSpirvAbi] for specifying these
|
||
interfaces:
|
||
|
||
* `spv.entry_point_abi` is a struct attribute that should be attached to the
|
||
entry function. It contains:
|
||
* `local_size` for specifying the local work group size for the dispatch.
|
||
* `spv.interface_var_abi` is attribute that should be attached to each operand
|
||
and result of the entry function. It should be of `#spv.interface_var_abi`
|
||
attribute kind, which is defined as:
|
||
|
||
```
|
||
spv-storage-class ::= `StorageBuffer` | ...
|
||
spv-descriptor-set ::= integer-literal
|
||
spv-binding ::= integer-literal
|
||
spv-interface-var-abi ::= `#` `spv.interface_var_abi` `<(` spv-descriptor-set
|
||
`,` spv-binding `)` (`,` spv-storage-class)? `>`
|
||
```
|
||
|
||
For example,
|
||
|
||
```
|
||
#spv.interface_var_abi<(0, 0), StorageBuffer>
|
||
#spv.interface_var_abi<(0, 1)>
|
||
```
|
||
|
||
The attribute has a few fields:
|
||
|
||
* Descriptor set number for the corresponding resource variable.
|
||
* Binding number for the corresponding resource variable.
|
||
* Storage class for the corresponding resource variable.
|
||
|
||
The SPIR-V dialect provides a [`LowerABIAttributesPass`][MlirSpirvPasses] that
|
||
uses this information to lower the entry point function and its ABI consistent
|
||
with the Vulkan validation rules. Specifically,
|
||
|
||
* Creates `spv.GlobalVariable`s for the arguments, and replaces all uses of
|
||
the argument with this variable. The SSA value used for replacement is
|
||
obtained using the `spv.mlir.addressof` operation.
|
||
* Adds the `spv.EntryPoint` and `spv.ExecutionMode` operations into the
|
||
`spv.module` for the entry function.
|
||
|
||
## Serialization and deserialization
|
||
|
||
Although the main objective of the SPIR-V dialect is to act as a proper IR for
|
||
compiler transformations, being able to serialize to and deserialize from the
|
||
binary format is still very valuable for many good reasons. Serialization
|
||
enables the artifacts of SPIR-V compilation to be consumed by an execution
|
||
environment; deserialization allows us to import SPIR-V binary modules and run
|
||
transformations on them. So serialization and deserialization are supported from
|
||
the very beginning of the development of the SPIR-V dialect.
|
||
|
||
The serialization library provides two entry points, `mlir::spirv::serialize()`
|
||
and `mlir::spirv::deserialize()`, for converting a MLIR SPIR-V module to binary
|
||
format and back. The [Code organization](#code-organization) explains more about
|
||
this.
|
||
|
||
Given that the focus is transformations, which inevitably means changes to the
|
||
binary module; so serialization is not designed to be a general tool for
|
||
investigating the SPIR-V binary module and does not guarantee roundtrip
|
||
equivalence (at least for now). For the latter, please use the
|
||
assembler/disassembler in the [SPIRV-Tools][SpirvTools] project.
|
||
|
||
A few transformations are performed in the process of serialization because of
|
||
the representational differences between SPIR-V dialect and binary format:
|
||
|
||
* Attributes on `spv.module` are emitted as their corresponding SPIR-V
|
||
instructions.
|
||
* Types are serialized into `OpType*` instructions in the SPIR-V binary module
|
||
section for types, constants, and global variables.
|
||
* `spv.Constant`s are unified and placed in the SPIR-V binary module section
|
||
for types, constants, and global variables.
|
||
* Attributes on ops, if not part of the op's binary encoding, are emitted as
|
||
`OpDecorate*` instructions in the SPIR-V binary module section for
|
||
decorations.
|
||
* `spv.mlir.selection`s and `spv.mlir.loop`s are emitted as basic blocks with `Op*Merge`
|
||
instructions in the header block as required by the binary format.
|
||
* Block arguments are materialized as `OpPhi` instructions at the beginning of
|
||
the corresponding blocks.
|
||
|
||
Similarly, a few transformations are performed during deserialization:
|
||
|
||
* Instructions for execution environment requirements (extensions,
|
||
capabilities, extended instruction sets, etc.) will be placed as attributes
|
||
on `spv.module`.
|
||
* `OpType*` instructions will be converted into proper `mlir::Type`s.
|
||
* `OpConstant*` instructions are materialized as `spv.Constant` at each use
|
||
site.
|
||
* `OpVariable` instructions will be converted to `spv.GlobalVariable` ops if
|
||
in module-level; otherwise they will be converted into `spv.Variable` ops.
|
||
* Every use of a module-level `OpVariable` instruction will materialize a
|
||
`spv.mlir.addressof` op to turn the symbol of the corresponding
|
||
`spv.GlobalVariable` into an SSA value.
|
||
* Every use of a `OpSpecConstant` instruction will materialize a
|
||
`spv.mlir.referenceof` op to turn the symbol of the corresponding
|
||
`spv.SpecConstant` into an SSA value.
|
||
* `OpPhi` instructions are converted to block arguments.
|
||
* Structured control flow are placed inside `spv.mlir.selection` and `spv.mlir.loop`.
|
||
|
||
## Conversions
|
||
|
||
One of the main features of MLIR is the ability to progressively lower from
|
||
dialects that capture programmer abstraction into dialects that are closer to a
|
||
machine representation, like SPIR-V dialect. This progressive lowering through
|
||
multiple dialects is enabled through the use of the
|
||
[DialectConversion][MlirDialectConversion] framework in MLIR. To simplify
|
||
targeting SPIR-V dialect using the Dialect Conversion framework, two utility
|
||
classes are provided.
|
||
|
||
(**Note** : While SPIR-V has some [validation rules][SpirvShaderValidation],
|
||
additional rules are imposed by [Vulkan execution environment][VulkanSpirv]. The
|
||
lowering described below implements both these requirements.)
|
||
|
||
### `SPIRVConversionTarget`
|
||
|
||
The `mlir::spirv::SPIRVConversionTarget` class derives from the
|
||
`mlir::ConversionTarget` class and serves as a utility to define a conversion
|
||
target satisfying a given [`spv.target_env`](#target-environment). It registers
|
||
proper hooks to check the dynamic legality of SPIR-V ops. Users can further
|
||
register other legality constraints into the returned `SPIRVConversionTarget`.
|
||
|
||
`spirv::lookupTargetEnvOrDefault()` is a handy utility function to query an
|
||
`spv.target_env` attached in the input IR or use the default to construct a
|
||
`SPIRVConversionTarget`.
|
||
|
||
### `SPIRVTypeConverter`
|
||
|
||
The `mlir::SPIRVTypeConverter` derives from `mlir::TypeConverter` and provides
|
||
type conversion for builtin types to SPIR-V types conforming to the
|
||
[target environment](#target-environment) it is constructed with. If the
|
||
required extension/capability for the resultant type is not available in the
|
||
given target environment, `convertType()` will return a null type.
|
||
|
||
Standard scalar types are converted to their corresponding SPIR-V scalar types.
|
||
|
||
(TODO: Note that if the bitwidth is not available in the target environment,
|
||
it will be unconditionally converted to 32-bit. This should be switched to
|
||
properly emulating non-32-bit scalar types.)
|
||
|
||
[Standard index type][MlirIndexType] need special handling since they are not
|
||
directly supported in SPIR-V. Currently the `index` type is converted to `i32`.
|
||
|
||
(TODO: Allow for configuring the integer width to use for `index` types in the
|
||
SPIR-V dialect)
|
||
|
||
SPIR-V only supports vectors of 2/3/4 elements; so
|
||
[standard vector types][MlirVectorType] of these lengths can be converted
|
||
directly.
|
||
|
||
(TODO: Convert other vectors of lengths to scalars or arrays)
|
||
|
||
[Standard memref types][MlirMemrefType] with static shape and stride are
|
||
converted to `spv.ptr<spv.struct<spv.array<...>>>`s. The resultant SPIR-V array
|
||
types have the same element type as the source memref and its number of elements
|
||
is obtained from the layout specification of the memref. The storage class of
|
||
the pointer type are derived from the memref's memory space with
|
||
`SPIRVTypeConverter::getStorageClassForMemorySpace()`.
|
||
|
||
### Utility functions for lowering
|
||
|
||
#### Setting layout for shader interface variables
|
||
|
||
SPIR-V validation rules for shaders require composite objects to be explicitly
|
||
laid out. If a `spv.GlobalVariable` is not explicitly laid out, the utility
|
||
method `mlir::spirv::decorateType` implements a layout consistent with
|
||
the [Vulkan shader requirements][VulkanShaderInterface].
|
||
|
||
#### Creating builtin variables
|
||
|
||
In SPIR-V dialect, builtins are represented using `spv.GlobalVariable`s, with
|
||
`spv.mlir.addressof` used to get a handle to the builtin as an SSA value. The
|
||
method `mlir::spirv::getBuiltinVariableValue` creates a `spv.GlobalVariable` for
|
||
the builtin in the current `spv.module` if it does not exist already, and
|
||
returns an SSA value generated from an `spv.mlir.addressof` operation.
|
||
|
||
### Current conversions to SPIR-V
|
||
|
||
Using the above infrastructure, conversions are implemented from
|
||
|
||
* [Standard Dialect][MlirStandardDialect] : Only arithmetic and logical
|
||
operations conversions are implemented.
|
||
* [GPU Dialect][MlirGpuDialect] : A gpu.module is converted to a `spv.module`.
|
||
A gpu.function within this module is lowered as an entry function.
|
||
|
||
## Code organization
|
||
|
||
We aim to provide multiple libraries with clear dependencies for SPIR-V related
|
||
functionalities in MLIR so developers can just choose the needed components
|
||
without pulling in the whole world.
|
||
|
||
### The dialect
|
||
|
||
The code for the SPIR-V dialect resides in a few places:
|
||
|
||
* Public headers are placed in [include/mlir/Dialect/SPIRV][MlirSpirvHeaders].
|
||
* Libraries are placed in [lib/Dialect/SPIRV][MlirSpirvLibs].
|
||
* IR tests are placed in [test/Dialect/SPIRV][MlirSpirvTests].
|
||
* Unit tests are placed in [unittests/Dialect/SPIRV][MlirSpirvUnittests].
|
||
|
||
The whole SPIR-V dialect is exposed via multiple headers for better
|
||
organization:
|
||
|
||
* [SPIRVDialect.h][MlirSpirvDialect] defines the SPIR-V dialect.
|
||
* [SPIRVTypes.h][MlirSpirvTypes] defines all SPIR-V specific types.
|
||
* [SPIRVOps.h][MlirSPirvOpsH] defines all SPIR-V operations.
|
||
* [Serialization.h][MlirSpirvSerialization] defines the entry points for
|
||
serialization and deserialization.
|
||
|
||
The dialect itself, including all types and ops, is in the `MLIRSPIRV` library.
|
||
Serialization functionalities are in the `MLIRSPIRVSerialization` library.
|
||
|
||
### Op definitions
|
||
|
||
We use [Op Definition Spec][ODS] to define all SPIR-V ops. They are written in
|
||
TableGen syntax and placed in various `*Ops.td` files in the header directory.
|
||
Those `*Ops.td` files are organized according to the instruction categories used
|
||
in the SPIR-V specification, for example, an op belonging to the "Atomics
|
||
Instructions" section is put in the `SPIRVAtomicOps.td` file.
|
||
|
||
`SPIRVOps.td` serves as the main op definition file that includes all files
|
||
for specific categories.
|
||
|
||
`SPIRVBase.td` defines common classes and utilities used by various op
|
||
definitions. It contains the TableGen SPIR-V dialect definition, SPIR-V
|
||
versions, known extensions, various SPIR-V enums, TableGen SPIR-V types, and
|
||
base op classes, etc.
|
||
|
||
Many of the contents in `SPIRVBase.td`, e.g., the opcodes and various enums, and
|
||
all `*Ops.td` files can be automatically updated via a Python script, which
|
||
queries the SPIR-V specification and grammar. This greatly reduces the burden of
|
||
supporting new ops and keeping updated with the SPIR-V spec. More details on
|
||
this automated development can be found in the
|
||
[Automated development flow](#automated-development-flow) section.
|
||
|
||
### Dialect conversions
|
||
|
||
The code for conversions from other dialects to the SPIR-V dialect also resides
|
||
in a few places:
|
||
|
||
* From GPU dialect: headers are at
|
||
[include/mlir/Conversion/GPUTOSPIRV][MlirGpuToSpirvHeaders]; libraries are
|
||
at [lib/Conversion/GPUToSPIRV][MlirGpuToSpirvLibs].
|
||
* From standard dialect: headers are at
|
||
[include/mlir/Conversion/StandardTOSPIRV][MlirStdToSpirvHeaders]; libraries
|
||
are at [lib/Conversion/StandardToSPIRV][MlirStdToSpirvLibs].
|
||
|
||
These dialect to dialect conversions have their dedicated libraries,
|
||
`MLIRGPUToSPIRV` and `MLIRStandardToSPIRV`, respectively.
|
||
|
||
There are also common utilities when targeting SPIR-V from any dialect:
|
||
|
||
* [include/mlir/Dialect/SPIRV/Transforms/SPIRVConversion.h][MlirSpirvConversion]
|
||
contains type converters and other utility functions.
|
||
* [include/mlir/Dialect/SPIRV/Transforms/Passes.h][MlirSpirvPasses] contains
|
||
SPIR-V specific analyses and transformations.
|
||
|
||
These common utilities are implemented in the `MLIRSPIRVConversion` and
|
||
`MLIRSPIRVTransforms` library, respectively.
|
||
|
||
## Rationale
|
||
|
||
### Lowering `memref`s to `!spv.array<..>` and `!spv.rtarray<..>`.
|
||
|
||
The LLVM dialect lowers `memref` types to a `MemrefDescriptor`:
|
||
|
||
```
|
||
struct MemrefDescriptor {
|
||
void *allocated_ptr; // Pointer to the base allocation.
|
||
void *aligned_ptr; // Pointer within base allocation which is aligned to
|
||
// the value set in the memref.
|
||
size_t offset; // Offset from aligned_ptr from where to get values
|
||
// corresponding to the memref.
|
||
size_t shape[rank]; // Shape of the memref.
|
||
size_t stride[rank]; // Strides used while accessing elements of the memref.
|
||
};
|
||
```
|
||
|
||
In SPIR-V dialect, we chose not to use a `MemrefDescriptor`. Instead a `memref`
|
||
is lowered directly to a `!spv.ptr<!spv.array<nelts x elem_type>>` when the
|
||
`memref` is statically shaped, and `!spv.ptr<!spv.rtarray<elem_type>>` when the
|
||
`memref` is dynamically shaped. The rationale behind this choice is described
|
||
below.
|
||
|
||
1. Inputs/output buffers to a SPIR-V kernel are specified using
|
||
[`OpVariable`][SpirvOpVariable] inside [interface storage
|
||
classes][VulkanShaderInterfaceStorageClass] (e.g., Uniform, StorageBuffer,
|
||
etc.), while kernel private variables reside in non-interface storage
|
||
classes (e.g., Function, Workgroup, etc.). By default, Vulkan-flavored
|
||
SPIR-V requires logical addressing mode: one cannot load/store pointers
|
||
from/to variables and cannot perform pointer arithmetic. Expressing a
|
||
struct like `MemrefDescriptor` in interface storage class requires special
|
||
addressing mode
|
||
([PhysicalStorageBuffer][VulkanExtensionPhysicalStorageBuffer]) and
|
||
manipulating such a struct in non-interface storage classes requires special
|
||
capabilities ([VariablePointers][VulkanExtensionVariablePointers]).
|
||
Requiring these two extensions together will significantly limit the
|
||
Vulkan-capable device we can target; basically ruling out mobile support..
|
||
|
||
1. An alternative to having one level of indirection (as is the case with
|
||
`MemrefDescriptor`s), is to embed the `!spv.array` or `!spv.rtarray`
|
||
directly in the `MemrefDescriptor`, Having such a descriptor at the ABI
|
||
boundary implies that the first few bytes of the input/output buffers would
|
||
need to be reserved for shape/stride information. This adds an unnecessary
|
||
burden on the host side.
|
||
|
||
1. A more performant approach would be to have the data be an `OpVariable`,
|
||
with the shape and strides passed using a separate `OpVariable`. This has
|
||
further advantages:
|
||
|
||
* All the dynamic shape/stride information of the `memref` can be combined
|
||
into a single descriptor. Descriptors are [limited resources on many
|
||
Vulkan hardware][VulkanGPUInfoMaxPerStageDescriptorStorageBuffers]. So
|
||
combining them would help make the generated code more portable across
|
||
devices.
|
||
* If the shape/stride information is small enough, they could be accessed
|
||
using [PushConstants][VulkanPushConstants] that are faster to access and
|
||
avoid buffer allocation overheads. These would be unnecessary if all
|
||
shapes are static. In the dynamic shape cases, a few parameters are
|
||
typically enough to compute the shape of all `memref`s used/referenced
|
||
within the kernel making the use of PushConstants possible.
|
||
* The shape/stride information (typically) needs to be update less
|
||
frequently than the data stored in the buffers. They could be part of
|
||
different descriptor sets.
|
||
|
||
## Contribution
|
||
|
||
All kinds of contributions are highly appreciated! :) We have GitHub issues for
|
||
tracking the [dialect][GitHubDialectTracking] and
|
||
[lowering][GitHubLoweringTracking] development. You can find todo tasks there.
|
||
The [Code organization](#code-organization) section gives an overview of how
|
||
SPIR-V related functionalities are implemented in MLIR. This section gives more
|
||
concrete steps on how to contribute.
|
||
|
||
### Automated development flow
|
||
|
||
One of the goals of SPIR-V dialect development is to leverage both the SPIR-V
|
||
[human-readable specification][SpirvSpec] and
|
||
[machine-readable grammar][SpirvGrammar] to auto-generate as much contents as
|
||
possible. Specifically, the following tasks can be automated (partially or
|
||
fully):
|
||
|
||
* Adding support for a new operation.
|
||
* Adding support for a new SPIR-V enum.
|
||
* Serialization and deserialization of a new operation.
|
||
|
||
We achieve this using the Python script
|
||
[`gen_spirv_dialect.py`][GenSpirvUtilsPy]. It fetches the human-readable
|
||
specification and machine-readable grammar directly from the Internet and
|
||
updates various SPIR-V `*.td` files in place. The script gives us an automated
|
||
flow for adding support for new ops or enums.
|
||
|
||
Afterwards, we have SPIR-V specific `mlir-tblgen` backends for reading the Op
|
||
Definition Spec and generate various components, including (de)serialization
|
||
logic for ops. Together with standard `mlir-tblgen` backends, we auto-generate
|
||
all op classes, enum classes, etc.
|
||
|
||
In the following subsections, we list the detailed steps to follow for common
|
||
tasks.
|
||
|
||
### Add a new op
|
||
|
||
To add a new op, invoke the `define_inst.sh` script wrapper in utils/spirv.
|
||
`define_inst.sh` requires a few parameters:
|
||
|
||
```sh
|
||
./define_inst.sh <filename> <base-class-name> <opname>
|
||
```
|
||
|
||
For example, to define the op for `OpIAdd`, invoke
|
||
|
||
```sh
|
||
./define_inst.sh SPIRVArithmeticOps.td ArithmeticBinaryOp OpIAdd
|
||
```
|
||
|
||
where `SPIRVArithmeticOps.td` is the filename for hosting the new op and
|
||
`ArithmeticBinaryOp` is the direct base class the newly defined op will derive
|
||
from.
|
||
|
||
Similarly, to define the op for `OpAtomicAnd`,
|
||
|
||
```sh
|
||
./define_inst.sh SPIRVAtomicOps.td AtomicUpdateWithValueOp OpAtomicAnd
|
||
```
|
||
|
||
Note that the generated SPIR-V op definition is just a best-effort template; it
|
||
is still expected to be updated to have more accurate traits, arguments, and
|
||
results.
|
||
|
||
It is also expected that a custom assembly form is defined for the new op,
|
||
which will require providing the parser and printer. The EBNF form of the
|
||
custom assembly should be described in the op's description and the parser
|
||
and printer should be placed in [`SPIRVOps.cpp`][MlirSpirvOpsCpp] with the
|
||
following signatures:
|
||
|
||
```c++
|
||
static ParseResult parse<spirv-op-symbol>Op(OpAsmParser &parser,
|
||
OperationState &state);
|
||
static void print(spirv::<spirv-op-symbol>Op op, OpAsmPrinter &printer);
|
||
```
|
||
|
||
See any existing op as an example.
|
||
|
||
Verification should be provided for the new op to cover all the rules described
|
||
in the SPIR-V specification. Choosing the proper ODS types and attribute kinds,
|
||
which can be found in [`SPIRVBase.td`][MlirSpirvBase], can help here. Still
|
||
sometimes we need to manually write additional verification logic in
|
||
[`SPIRVOps.cpp`][MlirSpirvOpsCpp] in a function with the following signature:
|
||
|
||
```c++
|
||
static LogicalResult verify(spirv::<spirv-op-symbol>Op op);
|
||
```
|
||
|
||
See any such function in [`SPIRVOps.cpp`][MlirSpirvOpsCpp] as an example.
|
||
|
||
If no additional verification is needed, one needs to add the following to
|
||
the op's Op Definition Spec:
|
||
|
||
```
|
||
let verifier = [{ return success(); }];
|
||
```
|
||
|
||
To suppress the requirement of the above C++ verification function.
|
||
|
||
Tests for the op's custom assembly form and verification should be added to
|
||
the proper file in test/Dialect/SPIRV/.
|
||
|
||
The generated op will automatically gain the logic for (de)serialization.
|
||
However, tests still need to be coupled with the change to make sure no
|
||
surprises. Serialization tests live in test/Dialect/SPIRV/Serialization.
|
||
|
||
### Add a new enum
|
||
|
||
To add a new enum, invoke the `define_enum.sh` script wrapper in utils/spirv.
|
||
`define_enum.sh` expects the following parameters:
|
||
|
||
```sh
|
||
./define_enum.sh <enum-class-name>
|
||
```
|
||
|
||
For example, to add the definition for SPIR-V storage class in to
|
||
`SPIRVBase.td`:
|
||
|
||
```sh
|
||
./define_enum.sh StorageClass
|
||
```
|
||
|
||
### Add a new custom type
|
||
|
||
SPIR-V specific types are defined in [`SPIRVTypes.h`][MlirSpirvTypes]. See
|
||
examples there and the [tutorial][CustomTypeAttrTutorial] for defining new
|
||
custom types.
|
||
|
||
### Add a new conversion
|
||
|
||
To add conversion for a type update the `mlir::spirv::SPIRVTypeConverter` to
|
||
return the converted type (must be a valid SPIR-V type). See [Type
|
||
Conversion][MlirDialectConversionTypeConversion] for more details.
|
||
|
||
To lower an operation into SPIR-V dialect, implement a [conversion
|
||
pattern][MlirDialectConversionRewritePattern]. If the conversion requires type
|
||
conversion as well, the pattern must inherit from the
|
||
`mlir::spirv::SPIRVOpLowering` class to get access to
|
||
`mlir::spirv::SPIRVTypeConverter`. If the operation has a region, [signature
|
||
conversion][MlirDialectConversionSignatureConversion] might be needed as well.
|
||
|
||
**Note**: The current validation rules of `spv.module` require that all
|
||
operations contained within its region are valid operations in the SPIR-V
|
||
dialect.
|
||
|
||
## Operation definitions
|
||
|
||
[include "Dialects/SPIRVOps.md"]
|
||
|
||
[Spirv]: https://www.khronos.org/registry/spir-v/
|
||
[SpirvSpec]: https://www.khronos.org/registry/spir-v/specs/unified1/SPIRV.html
|
||
[SpirvLogicalLayout]: https://www.khronos.org/registry/spir-v/specs/unified1/SPIRV.html#_a_id_logicallayout_a_logical_layout_of_a_module
|
||
[SpirvGrammar]: https://raw.githubusercontent.com/KhronosGroup/SPIRV-Headers/master/include/spirv/unified1/spirv.core.grammar.json
|
||
[SpirvShaderValidation]: https://www.khronos.org/registry/spir-v/specs/unified1/SPIRV.html#_a_id_shadervalidation_a_validation_rules_for_shader_a_href_capability_capabilities_a
|
||
[SpirvOpVariable]: https://www.khronos.org/registry/spir-v/specs/unified1/SPIRV.html#OpVariable
|
||
[GlslStd450]: https://www.khronos.org/registry/spir-v/specs/1.0/GLSL.std.450.html
|
||
[ArrayType]: https://www.khronos.org/registry/spir-v/specs/unified1/SPIRV.html#OpTypeArray
|
||
[ImageType]: https://www.khronos.org/registry/spir-v/specs/unified1/SPIRV.html#OpTypeImage
|
||
[PointerType]: https://www.khronos.org/registry/spir-v/specs/unified1/SPIRV.html#OpTypePointer
|
||
[RuntimeArrayType]: https://www.khronos.org/registry/spir-v/specs/unified1/SPIRV.html#OpTypeRuntimeArray
|
||
[SampledImageType]: https://www.khronos.org/registry/spir-v/specs/unified1/SPIRV.html#OpTypeSampledImage
|
||
[MlirDialectConversion]: ../DialectConversion.md
|
||
[StructType]: https://www.khronos.org/registry/spir-v/specs/unified1/SPIRV.html#Structure
|
||
[SpirvTools]: https://github.com/KhronosGroup/SPIRV-Tools
|
||
[Rationale]: ../Rationale/Rationale.md/#block-arguments-vs-phi-nodes
|
||
[ODS]: ../OpDefinitions.md
|
||
[GreedyPatternRewriter]: https://github.com/llvm/llvm-project/blob/main/mlir/lib/Transforms/Utils/GreedyPatternRewriteDriver.cpp
|
||
[MlirDialectConversionTypeConversion]: ../DialectConversion.md/#type-converter
|
||
[MlirDialectConversionRewritePattern]: ../DialectConversion.md/#conversion-patterns
|
||
[MlirDialectConversionSignatureConversion]: ../DialectConversion.md/#region-signature-conversion
|
||
[MlirOpInterface]: ../Interfaces/#operation-interfaces
|
||
[MlirIntegerType]: Builtin.md/#integertype
|
||
[MlirVectorType]: Builtin.md/#vectortype
|
||
[MlirMemrefType]: Builtin.md/#memreftype
|
||
[MlirIndexType]: Builtin.md/#indextype
|
||
[MlirGpuDialect]: GPU.md
|
||
[MlirStandardDialect]: Standard.md
|
||
[MlirSpirvHeaders]: https://github.com/llvm/llvm-project/tree/main/mlir/include/mlir/Dialect/SPIRV
|
||
[MlirSpirvLibs]: https://github.com/llvm/llvm-project/tree/main/mlir/lib/Dialect/SPIRV
|
||
[MlirSpirvTests]: https://github.com/llvm/llvm-project/tree/main/mlir/test/Dialect/SPIRV
|
||
[MlirSpirvUnittests]: https://github.com/llvm/llvm-project/tree/main/mlir/unittests/Dialect/SPIRV
|
||
[MlirGpuToSpirvHeaders]: https://github.com/llvm/llvm-project/tree/main/mlir/include/mlir/Conversion/GPUToSPIRV
|
||
[MlirGpuToSpirvLibs]: https://github.com/llvm/llvm-project/tree/main/mlir/lib/Conversion/GPUToSPIRV
|
||
[MlirStdToSpirvHeaders]: https://github.com/llvm/llvm-project/tree/main/mlir/include/mlir/Conversion/StandardToSPIRV
|
||
[MlirStdToSpirvLibs]: https://github.com/llvm/llvm-project/tree/main/mlir/lib/Conversion/StandardToSPIRV
|
||
[MlirSpirvDialect]: https://github.com/llvm/llvm-project/blob/main/mlir/include/mlir/Dialect/SPIRV/IR/SPIRVDialect.h
|
||
[MlirSpirvTypes]: https://github.com/llvm/llvm-project/blob/main/mlir/include/mlir/Dialect/SPIRV/IR/SPIRVTypes.h
|
||
[MlirSpirvOpsH]: https://github.com/llvm/llvm-project/blob/main/mlir/include/mlir/Dialect/SPIRV/IR/SPIRVOps.h
|
||
[MlirSpirvSerialization]: https://github.com/llvm/llvm-project/blob/main/mlir/include/mlir/Target/SPIRV/Serialization.h
|
||
[MlirSpirvBase]: https://github.com/llvm/llvm-project/blob/main/mlir/include/mlir/Dialect/SPIRV/IR/SPIRVBase.td
|
||
[MlirSpirvPasses]: https://github.com/llvm/llvm-project/blob/main/mlir/include/mlir/Dialect/SPIRV/Transforms/Passes.h
|
||
[MlirSpirvConversion]: https://github.com/llvm/llvm-project/blob/main/mlir/include/mlir/Dialect/SPIRV/Transforms/SPIRVConversion.h
|
||
[MlirSpirvAbi]: https://github.com/llvm/llvm-project/blob/main/mlir/include/mlir/Dialect/SPIRV/IR/TargetAndABI.h
|
||
[MlirSpirvOpsCpp]: https://github.com/llvm/llvm-project/blob/main/mlir/lib/Dialect/SPIRV/IR/SPIRVOps.cpp
|
||
[GitHubDialectTracking]: https://github.com/tensorflow/mlir/issues/302
|
||
[GitHubLoweringTracking]: https://github.com/tensorflow/mlir/issues/303
|
||
[GenSpirvUtilsPy]: https://github.com/llvm/llvm-project/blob/main/mlir/utils/spirv/gen_spirv_dialect.py
|
||
[CustomTypeAttrTutorial]: ../Tutorials/DefiningAttributesAndTypes.md
|
||
[VulkanExtensionPhysicalStorageBuffer]: https://github.com/KhronosGroup/SPIRV-Registry/blob/main/extensions/KHR/SPV_KHR_physical_storage_buffer.html
|
||
[VulkanExtensionVariablePointers]: https://github.com/KhronosGroup/SPIRV-Registry/blob/main/extensions/KHR/SPV_KHR_variable_pointers.html
|
||
[VulkanSpirv]: https://renderdoc.org/vkspec_chunked/chap40.html#spirvenv
|
||
[VulkanShaderInterface]: https://renderdoc.org/vkspec_chunked/chap14.html#interfaces-resources
|
||
[VulkanShaderInterfaceStorageClass]: https://renderdoc.org/vkspec_chunked/chap15.html#interfaces
|
||
[VulkanResourceLimits]: https://renderdoc.org/vkspec_chunked/chap36.html#limits
|
||
[VulkanGPUInfoMaxPerStageDescriptorStorageBuffers]: https://vulkan.gpuinfo.org/displaydevicelimit.php?name=maxPerStageDescriptorStorageBuffers&platform=android
|
||
[VulkanPushConstants]: https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/vkCmdPushConstants.html
|