SPIR-V Dialect

This document defines the SPIR-V dialect in MLIR.

SPIR-V 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.

Design Principles

SPIR-V defines a stable binary format for hardware driver consumption. 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 driver consumption but not necessarily compiler transformations.

The purpose of the SPIR-V dialect is to serve as the “proxy” of the binary format and to facilitate transformations. Therefore, it should

  • Be trivial to serialize into the SPIR-V binary format;
  • Stay as the same semantic level and try to be a mechanical 1:1 mapping;
  • But deviate representationally if possible with MLIR mechanisms.

Conventions

The SPIR-V dialect has the following conventions:

  • The prefix for all SPIR-V types and operations are spv..
  • Ops that directly correspond to instructions in the binary format have CamelCase names, for example, spv.FMul;
  • Otherwise they have snake_case names. These ops are mostly for defining the SPIR-V structure, inclduing module, function, and module-level ops. For example, spv.module, spv.constant.

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 standard func op.

Compared to the binary format, we adjust how certain module-level SPIR-V instructions are represented in the SPIR-V dialect. Notably,

  • Requirements for capabilities, extensions, extended instruction sets, addressing model, and memory model is conveyed using spv.module attributes. This is considered better because these information are for the exexcution environment. It's eaiser to probe them if on the module op itself.
  • Annotations/decoration instrutions are “folded” into the instructions they decorate and represented as attributes on those ops. This elimiates potential forward references of SSA values, improves IR readability, and makes querying the annotations more direct.
  • Various 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.

Types

The SPIR-V dialect reuses standard integer, float, and vector types and defines the following dialect-specific types:

spirv-type ::= array-type
             | pointer-type
             | runtime-array-type

Array type

This corresponds to SPIR-V array type. Its syntax is

element-type ::= integer-type
               | floating-point-type
               | vector-type
               | spirv-type

array-type ::= `!spv.array<` integer-literal `x` element-type `>`

For example,

!spv.array<4 x i32>
!spv.array<16 x vector<4 x f32>>

Image type

This corresponds to SPIR-V image type. 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,

!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. Its syntax is

storage-class ::= `UniformConstant`
                | `Uniform`
                | `Workgroup`
                | <and other storage classes...>

pointer-type ::= `!spv.ptr<` element-type `,` storage-class `>`

For example,

!spv.ptr<i32, Function>
!spv.ptr<vector<4 x f32>, Uniform>

Runtime array type

This corresponds to SPIR-V runtime array type. Its syntax is

runtime-array-type ::= `!spv.rtarray<` element-type `>`

For example,

!spv.rtarray<i32>
!spv.rtarray<vector<4 x f32>>

Struct type

This corresponds to SPIR-V struct type. Its syntax is

struct-type ::= `!spv.struct<` spirv-type (` [` integer-literal `]` )?
                (`, ` spirv-type ( ` [` integer-literal `] ` )? )* `>`

For Example,

!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]>

Serialization

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 purpose of this library is to enable importing SPIR-V binary modules to run transformations on them and exporting SPIR-V modules to be consumed by execution environments. The focus is transformations, which inevitably means changes to the binary module; so it 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 project.