Skip to content

Instantly share code, notes, and snippets.

@zeux
Last active July 16, 2021 01:35
Show Gist options
  • Save zeux/c83001968e06fe0b789fa4bd513860c6 to your computer and use it in GitHub Desktop.
Save zeux/c83001968e06fe0b789fa4bd513860c6 to your computer and use it in GitHub Desktop.
Notes on supporting half-precision computation in SPIRV based shader pipelines

The following assumes that HLSL source is used as an input for shader compilation, glslang is used to compile it to SPIRV, and then SPIRV-Cross is optionally used to compile the result to MSL/GLSL.

First, make sure you use min16floatN type instead of halfN type. This is necessary because glslang treats halfN type (with default compilation options) as floatN because unfortunately that's what DX10 does as well.

#define half min16float
#define half2 min16float2
#define half3 min16float3
#define half4 min16float4

Now, you have two options when compiling HLSL to SPIRV: default settings, and --hlsl-enable-16bit-types (aka EShMsgHlslEnable16BitTypes if you invoke glslang via C++).

By default, min16floatN types compile to floatN types in SPIRV, but the variables, including temporaries, will have RelaxedPrecision annotation in SPIRV:

OpDecorate %27 RelaxedPrecision

This is the behavior that you probably want when compiling to SPIRV directly for use in Vulkan! The decoration is optional, so devices that don't support 16-bit floating point computation are free to disregard this and use 32-bit floating point math instead.

You can also use SPIRV-Cross to cross-compile this to GLSL:

#version 450 es
precision mediump float;
precision highp int;

layout(location = 0) in vec4 input_color;
layout(location = 0) out vec4 _entryPointOutput;

(vec4 in the code above refers to mediump vec4; ordinarily SPIRV-Cross would insert highp vec4)

Unfortunately, SPIRV-Cross doesn't support RelaxedPrecision for MSL. To get around that, you need to enable 16-bit type support when building SPIRV for use with SPIRV-Cross, which will result in the actual half-precision types being emitted:

%half = OpTypeFloat 16
%v4half = OpTypeVector %half 4

Note that you can't use the resulting SPIRV in Vulkan if the device doesn't support VK_KHR_shader_float16_int8! Most older drivers don't. However, we don't care about this - we care about feeding this to SPIRV-Cross, which will happily translate this to halfN types when targeting Metal:

half4 _entryPointOutput [[color(0)]];

The danger of this approach is that using half in constant buffers will result in actual half-precision types (with sizeof=2) emitted for Metal but not for other APIs, which will result in a different constant buffer layout. This can be prevented by querying SPIRV-Cross reflection information and making sure half types aren't used for uniform data.

@greg-lunarg
Copy link

Unfortunately, SPIRV-Cross doesn't support RelaxedPrecision for MSL. To get around that, you need to enable 16-bit type support when building SPIRV for use with SPIRV-Cross, which will result in the actual half-precision types being emitted:

You seem to be implying that there is an automated way to get from GLSL with mediump floats to SPIRV with explicit half types. Am I understanding correctly? If so, what is the command to do that?

@zeux
Copy link
Author

zeux commented Jun 13, 2019

@greg-lunarg I don’t think this is possible with GLSL source; when HLSL is used instead, —hlsl-enable-16bit-types in glslang and - I think - —enable-16bit-types in dxc will do that.

@greg-lunarg
Copy link

OK. I have an HLSL shader with min16float types replacing all float types.

When compiling with glslang without --hlsl-enable-16-types, the compile completes without error and I see float types with RelaxedPrecision decorations in the SPIR-V as one would expect.

When compiling with glslang with --hlsl-enable-16-types, the compile complains about an unimplemented texture type and then missing conversion operators. Basically, it seems like, under this option, glslang is replacing all min16float types with half types. The problem is that half types are not supported everywhere min16floats are, and extensive editing including addition of explicit conversions is required.

When compiling with dxc without --enable-16-types, the compile completes without error and I see float types with RelaxedPrecision decorations in the SPIR-V as one would expect.

When compiling with dxc with --enable-16-types, the compile completes without error and the SPIR-V seems identical to that without the option, that is, I do not see half types, but rather float types with RelaxedPrecision.

So I am not seeing a completely automated path from HLSL with min16float types to "equivalent" SPIR-V only with explicit half types, suitable to be converted to MSL. Please let me know if I am missing something.

I apologize if I seem pedantic here, but I am in the middle of creating a pass in spirv-opt which will essentially reduce an HLSL shader's float precision to half wherever possible only using half precision types, inserting converts between float and half where needed, suitable to be fed to MSL.

Your thread raised questions as to whether such a pass would be useful.

If there already existed such an automated path, we could replace all floats in HLSL shaders with min16floats, follow the automated path and we would be done.

But in the absence of such an automated path, I still believe that the spirv-opt pass would be useful.

@zeux
Copy link
Author

zeux commented Jun 13, 2019

So I am not seeing a completely automated path from HLSL with min16float types to "equivalent" SPIR-V only with explicit half types, suitable to be converted to MSL. Please let me know if I am missing something.

I definitely haven't fully explored this! I experimented a bit with targeted application of half - that is, my usecase isn't converting the entire shader to half precision, it's strategically choosing parts of the shader that can tolerate half-precision, and having this choice carry over to all of Vulkan/Metal/OpenGL from a single HLSL source.

Is the issue that Texture2D or something along these lines is supported but Texture2D isn't?

@zeux
Copy link
Author

zeux commented Jun 13, 2019

I apologize if I seem pedantic here, but I am in the middle of creating a pass in spirv-opt which will essentially reduce an HLSL shader's float precision to half wherever possible only using half precision types, inserting converts between float and half where needed, suitable to be fed to MSL.

I wasn't aware of this - it definitely would be valuable! The document above documents the behavior I've observed, it's definitely not ideal. For example, if your pass could be used to replace float to half only when RelaxedPrecision is used, then the process of compiling shaders with relaxed precision to Metal could be more straightforward that it is now. The note I left about half-precision types inside uniform buffers would not be important if such a pass converted temporaries but left uniform structures alone.

@greg-lunarg
Copy link

Yes, sorry we have not advertised it yet. Right now we are in the proof-of-concept phase to see if such a thing might be profitable. One concern is is if the cost of conversions outweighs the savings in computation.

We hope to have some data in the next week or so.

@greg-lunarg
Copy link

Is the issue that Texture2D or something along these lines is supported but Texture2D isn't?

Typo?

@zeux
Copy link
Author

zeux commented Jun 13, 2019

Tags eaten by HTML :( I meant Texture2D<float4> vs Texture2D<min16float4>

@greg-lunarg
Copy link

Texture2D<min16float4> is supported (probably as Texture2D<float4>).

Texture2D<half4> is not supported by glslang, dxc, or apparently Vulkan.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment