实现 FP16 的第一步

首次发布:
Tom Hammersley's avatar
Tom Hammersley

半精度 (FP16) 计算是一种性能增强型 GPU 技术,长期以来在主机和移动设备中得到广泛应用,但在主流 PC 开发中并不常用或易于获得。随着 AMD Vega GPU 架构的问世,这项技术现在更容易获得,可以提升主流 PC 开发中的图形性能。

最新版的 GCN 架构允许您将 2 个 FP16 值打包到每个 32 位 VGPR 寄存器中。这使您能够

  • 通过使用新的数据并行指令,将 ALU 操作减半。
  • 减小着色器的 VGPR 占用空间,从而可能提高占用率和性能。

也存在一些小风险

  • 对 FP16 的使用不当可能导致 FP16 和 FP32 之间进行过多的转换。这会降低性能优势。
  • FP16 会稍微增加代码的复杂性和维护难度。

入门

很容易认为实现 FP16 就像简单地用“half”类型替换“float”一样。这行不通:这在 PC 上根本不起作用。DirectX® FXC 编译器仅提供 `half` 用于兼容性;它将其映射到 `float`。如果您比较生成的字节码,它们是相同的。

正确使用的类型是标准 HLSL 类型,前面加上 `min16` 前缀:`min16float`、`min16int`、`min16uint`。这些可以像往常一样用作标量或向量类型。

您的开发环境需要特定的软件才能成功生成 FP16 代码。首先,您需要 Windows 8 或更高版本。如果使用 `min16float`,旧版本的 Windows 将无法创建着色器。虽然 Windows 7 有一个平台更新可以启用 FP16 着色器的编译,但它只是将代码编译为 FP32。实际上,您是在模拟不存在的硬件,因此生成的代码可能不如预期高效。因此,为缺乏 FP16 支持的硬件和操作系统提供备用代码路径或着色器集可能是有益的。

其次,您需要最新版本的 FXC 编译器和驱动程序编译器。Windows 10 SDK 中的 FXC 编译器就足够了,并且需要 Radeon Crimson 驱动程序版本 17.9.1 或更高版本。

第三,值得澄清的是,FP16 将在 DirectX 11.1 和 Shader Model 5 代码中工作。不需要 DirectX 12。只需添加一个运行时测试,从 `ID3D11Device::CheckFeatureSupport()` 查询 `D3D11_FEATURE_SHADER_MIN_PRECISION_SUPPORT` 即可。

最重要的是,需要兼容的硬件:AMD RX Vega 或 Vega Frontier Edition GPU!

建议:预处理器支持

虽然 `min16float` 是完全合法的 HLSL 语法,因此可以直接使用,但我建议避免直接使用它。我认为最好实现预处理器支持,以便全局包含或排除 `min16float` 的使用,从而实现 FP16。原因有两个:

  1. 您需要能够完全移除它,以兼容旧操作系统或不兼容的硬件。
  2. 它提供了一种便捷的方式来执行性能或正确性的 A/B 测试。

示例输出

在准备好所有这些工具后,编译后的 FP16 代码是什么样的?让我们编写一个简单的测试函数。

cbuffer params
{
min16float4 colour;
};
Texture2D<min16float4> tex;
SamplerState samp;
min16float4 test( in min16float2 uv : TEXCOORD0 ) : SV_Target
{
return colour * tex.Sample( samp, uv );
}</min16float4>

验证 FP16 功能的第一步是查看 FXC 的 .asm 输出。驱动程序在收到正确的 DirectX 字节码之前,无法编译 FP16 代码。在这里,我们看到编译器引入了一系列 `{min16f}` 后缀。

ps_5_0
dcl_globalFlags refactoringAllowed | enableMinimumPrecision
dcl_constantbuffer CB0[1], immediateIndexed
dcl_sampler s0, mode_default
dcl_resource_texture2d (float,float,float,float) t0
dcl_input_ps linear v0.xy {min16f}
dcl_output o0.xyzw {min16f}
dcl_temps 1
sample_indexable(texture2d)(float,float,float,float) r0.xyzw {min16f}, v0.xyxx {min16f}, t0.xyzw, s0
mul o0.xyzw {min16f}, r0.xyzw {min16f}, cb0[0].xyzw {min16f}
ret

现在我们来看 ISA 输出。通常有两大类指令需要关注:

  • 打包 FP16 指令,例如 `v_pk_add/mul/sub/mad_f16`。
  • 混合指令,例如 `v_mad_mix_f32/v_mad_mixlo_f16/v_mad_mixhi_f16`。

`v_pk_add/mul/sub_f16` 等指令一次对两个 FP16 值执行 ALU 操作,从而将所需的指令和 ALU 时间减半。这是 FP16 的主要性能优势之一。

`mix` 修饰符允许您在一个 `VOP3` 指令中自由混合 FP16 和 FP32 操作数,而无需额外的转换指令。使用这些指令的成本是失去了发出打包指令的机会。因此,它不比您本来会执行的等效 FP32 指令更快或更慢。

请注意,混合指令的具体形式是乘加指令。通过创造性地使用 0、1 或 -1 常量,编译器可以利用它来实现大多数算术运算。然而,常见的着色器 ALU 操作,如 `min()` 或 `max()`,无法使用混合指令执行。

这是上述着色器的 GCN ISA 输出:

shader main
asic(GFX9)
type(PS)
s_mov_b32 m0, s20
s_mov_b64 s[22:23], exec
s_wqm_b64 exec, exec
s_setreg_imm32_b32 hwreg(HW_REG_MODE, 0, 8), 0x000001cc
v_interp_p1ll_f16 v2, v0, attr0.x
v_interp_p1ll_f16 v0, v0, attr0.y
v_interp_p2_f16 v2, v1, attr0.x, v2
v_interp_p2_f16 v2, v1, attr0.y, v0 op_sel:[0,0,0,1]
image_sample v[0:3], v[2:4], s[4:11], s[12:15] dmask:0xf a16 d16
s_buffer_load_dwordx4 s[0:3], s[16:19], 0x00
s_waitcnt lgkmcnt(0)
v_mov_b32 v2, s1
v_cvt_pkrtz_f16_f32 v2, s0, v2
v_mov_b32 v3, s3
v_cvt_pkrtz_f16_f32 v3, s2, v3
s_setreg_imm32_b32 hwreg(HW_REG_MODE, 0, 8), 0x000001c0
s_waitcnt vmcnt(0)
v_pk_mul_f16 v0, v0, v2 op_sel_hi:[1,1]
v_pk_mul_f16 v1, v1, v3 op_sel_hi:[1,1]
v_mov_b32 v2, v0 src0_sel: WORD_0
v_mov_b32 v0, v0 src0_sel: WORD_1
v_mov_b32 v3, v1 src0_sel: WORD_0
v_mov_b32 v1, v1 src0_sel: WORD_1
s_mov_b64 exec, s[22:23]
v_lshl_or_b32 v0, v0, 16, v2
v_lshl_or_b32 v1, v1, 16, v3
exp mrt0, v0, v0, v1, v1 done compr vm
s_endpgm
end

此输出说明了几个有趣的点。首先,编译器成功引入了一些 `v_pk_mul_f16` 指令。与通常将 float4 与标量相乘所需的四个 `v_mul_f32` 操作相比,我们将其减少到两个 `v_mul_pk_f16` 操作。

其次,考虑两个 `v_cvt_pkrtz` 指令。这些操作接收 2 个 FP32 源值,并将它们打包到单个 32 位目标寄存器中的 2 个 FP16 值。它这样做是为了在 cbuffer 中形成 `min16float4`。令人惊讶的是,尽管使用了正确的类型,编译器并未生成我们可能预期的简单加载。我们稍后将回到这个问题。

建议:Radeon GPU Analyzer

AMD 提供了一个极其强大的软件工具,称为 Radeon GPU Analyzer (RGA)。该工具是驱动程序编译器的接口,允许您直接查看生成的代码。RGA 接受所有主要图形 API 的着色器源代码或中间代码。用户指定要定位的 GCN GPU 代,该工具可以输出多种分析结果,包括但不限于 ISA 输出和寄存器使用情况分析。

我认为 RGA 对于 FP16 工作非常宝贵。我们将此工具集成到我们的工具链中,以便在编译后立即获得 ISA 输出或寄存器分析。我反复迭代 ISA 输出,直到获得满意的代码,然后测试其性能和正确性。虽然一些 GPU 捕获工具现在提供 ISA 反汇编,但这是一种更具生产力的方法。

FP16 目标选择

仔细选择目标至关重要。并非所有代码都适合 FP16 优化。理想目标

  • 兼容 FP16 的精度限制。
  • 提供良好的数据并行性。
  • 完全或部分受限于
    • ALU 操作的数量,或
    • 整体 VGPR 寄存器占用空间。

数据并行性通常有两种形式。打包指令可以轻松地用于处理 2、3 或 4 个组件向量的代码。或者,严格的标量代码可以通过手动展开循环并处理数据对来使其适用于打包指令。

常见目标

FP16 优化的可靠目标是颜色和法线贴图的混合。这些操作通常涉及大量数据并行 ALU 操作。此外,此类数据经常来自低精度纹理,因此可以很好地满足 FP16 的限制。典型的游戏帧在 gbuffer 导出和后处理着色器中有大量的这些操作,都可以进行优化。

BRDF 是一个有吸引力但困难的候选。计算镜面反射的部分通常非常占用寄存器和 ALU。这似乎是一个有希望的目标。然而,必须谨慎行事。BRDF 通常包含指数和除法运算。目前没有用于这些运算的 FP16 指令。这意味着,最多不会有这些运算的并行化;最坏的情况是,它会在 FP16 和 FP32 之间引入转换开销。

并非所有希望都渺茫。典型的 BRDF 方程中存在一个合适的优化候选:通常存在大量的向量和点积。虽然单个点积更多地是一种数据约简操作而不是数据并行操作,但可以使用 SIMD 代码并行执行许多点积。这些点积通常会反馈到 FP32 BRDF 代码中,因此必须小心,不要引入超过所节省的 FP16 到 FP32 转换开销。

最后,TAA 或棋盘格系统提供了强大的优化潜力,但也伴随着令人惊讶的风险。这些系统执行大量的颜色处理,ALU 确实可能是主要瓶颈。UV 计算通常会消耗 ALU 工作的大部分。很容易认为这些屏幕空间 UV 在 FP16 的限制范围内。令人惊讶的是,小像素速度和 4K 等高分辨率的组合在使用 FP16 时可能导致伪影。优化类似代码时请务必小心。

常量

编写 FP16 代码最有效的方法是为其提供 FP16 常量数据。任何使用 FP32 常量数据的操作都会触发转换操作。常量数据通常有两种形式:cbuffer 值和字面量。

理想情况下,每个 cbuffer 值都会有一个 FP16 版本可供使用。实际上,仅使用 FP32 cbuffer 数据通常就可以获得性能优势。这取决于常量使用的频率。如果一个常量只使用一两次,使用混合指令的开销并不大。如果常量使用更广泛,或用于向量,通常提供 FP16 cbuffer 值更有效。显然,像向量或矩阵这样的大型类型应作为本机 FP16 数据提供,因为转换开销将是过高的。

第二种常量数据来源是着色器中使用字面量。很容易认为使用 h 后缀足以引入 FP16 常量。事实并非如此。同样,half 类型是为了向后兼容,FXC 会将其转换为 FP32 字面量。使用 h 或 f 后缀都会导致转换。最好使用未加修饰的字面量,例如 0.0、1.5 等。通常,编译器可以根据上下文自动将该字面量编码为适当的 FP32 或 FP16。

一个例外是扩展字面量以用于向量操作。有时编译器无法自动将字面量扩展为 `min16float3`。在这种情况下,您必须手动构造一个 `min16float3`,或使用 `1.5.xxx` 等语法。

加载 FP16 数据

回想一下之前的示例代码片段。虽然编译器发出了预期的 `v_pk_mul_f16` 操作,但它并没有发出您可能期望从内存加载 `min16float4` 的代码序列。它加载了 FP32 值,并手动将它们打包成一个 FP16 向量。如果您要访问更大的类型,例如 min16float4x4 矩阵,代码序列将非常不理想。有一个简单的解决方案。如果我们更改源代码为

cbuffer params
{
uint2 packedColour;
};
Texture2D<min16float4> tex;
SamplerState samp;
min16float2 UnpackFloat16( uint a )
{
float2 tmp = f16tof32( uint2( a & 0xFFFF, a >> 16 ) );
return min16float2( tmp );
}
min16float4 UnpackFloat16( uint2 v )
{
return min16float4( UnpackFloat16( v.x ), UnpackFloat16( v.y ) );
}
min16float4 test( in min16float2 uv : TEXCOORD0 ) : SV_Target
{
min16float4 colour = UnpackFloat16( packedColour );
return colour * tex.Sample( samp, uv );
}

驱动程序识别此代码序列,并发出更优化的指令序列。

shader main
asic(GFX9)
type(PS)
s_mov_b32 m0, s20
s_mov_b64 s[2:3], exec
s_wqm_b64 exec, exec
s_setreg_imm32_b32 hwreg(HW_REG_MODE, 0, 8), 0x000001cc
v_interp_p1ll_f16 v2, v0, attr0.x
v_interp_p1ll_f16 v0, v0, attr0.y
v_interp_p2_f16 v2, v1, attr0.x, v2
v_interp_p2_f16 v2, v1, attr0.y, v0 op_sel:[0,0,0,1]
image_sample v[0:3], v[2:4], s[4:11], s[12:15] dmask:0xf a16 d16
s_buffer_load_dwordx2 s[0:1], s[16:19], 0x00
s_setreg_imm32_b32 hwreg(HW_REG_MODE, 0, 8), 0x000001c0
s_waitcnt vmcnt(0) & lgkmcnt(0)
v_pk_mul_f16 v0, v0, s0 op_sel_hi:[1,1]
v_pk_mul_f16 v1, v1, s1 op_sel_hi:[1,1]
v_mov_b32 v2, v0 src0_sel: WORD_0
v_mov_b32 v0, v0 src0_sel: WORD_1
v_mov_b32 v3, v1 src0_sel: WORD_0
v_mov_b32 v1, v1 src0_sel: WORD_1
s_mov_b64 exec, s[2:3]
v_lshl_or_b32 v0, v0, 16, v2
v_lshl_or_b32 v1, v1, 16, v3
exp mrt0, v0, v0, v1, v1 done compr vm
s_endpgm
end

最后,将 FP16 常量嵌入 cbuffer 的末尾而不是与 FP32 常量混合会很有用。这使得为非 FP16 兼容路径剥离 FP16 常量更加容易,对 cbuffer 大小、布局和 C++ 及着色器代码的成员对齐影响最小。

值得注意的是,Shader Model 6.2 支持所有内存操作的 16 位标量类型,这意味着上述问题将来会消失!

挑战

FP16 优化通常会遇到两个主要问题:

  • FP16 和 FP32 之间的转换开销。
  • 代码复杂性。

目前,FP16 通常是在着色器后期引入以提高其性能的。新的 FP16 代码需要转换指令才能集成并与 FP32 代码共存。程序员必须小心确保这些指令不会抵消或超过节省的时间。重要的是将大型计算块保持为纯 FP16 或 FP32,以限制此开销。事实上,像后处理或 gbuffer 导出这样的着色器可以完全以 FP16 模式运行。

这就引出了最后一点。FP16 代码为着色器代码增加了一些额外的复杂性。本文概述了最小化转换开销、解包 FP16 数据的特殊代码以及维护非 FP16 代码路径等问题。虽然这些问题很容易克服,但它们可能会使代码的编写和维护更加费力。记住,回报是非常值得的。

结论

FP16 是程序员工具箱中用于获得最佳着色器性能的宝贵附加工具。我们在 AMD RX Vega 硬件上观察到了约 10% 的收益。这是对中等工程投入具有吸引力和持久的回报。

资源

从 GitHub 获取 RGA

Radeon™ GPU Analyzer

Radeon GPU Analyzer 是一款用于 DirectX®、Vulkan®、SPIR-V™、OpenGL® 和 OpenCL™ 的离线编译器和性能分析工具。

将 Radeon™ GPU Analyzer 与 DirectX®12 图形结合使用

随着 DirectX 12 的出现,您可以生成最接近真实情况的反汇编和硬件资源使用统计数据,从而做出更好的性能优化决策。

AMD Radeon GPU Analyzer VS Code Extension

Radeon™ GPU Analyzer – Visual Studio® Code 扩展

这是 Radeon GPU Analyzer (RGA) 的 Visual Studio® Code 扩展,允许您直接在 VS Code 中使用 RGA。

Tom Hammersley 的其他嘉宾文章

将 AMD FidelityFX™ 集成到 Ego Engine

Codemasters 的 Tom Hammersley 谈论了将 FidelityFX 集成到 Ego Engine 和实现对比度自适应锐化 (CAS)。

Tom Hammersley's avatar

Tom Hammersley

Tom 是 Codemasters Software 的首席程序员,负责 F1 系列游戏。他在游戏行业拥有超过 20 年的经验,涵盖所有 PC 和主机代。Tom 专注于所有平台、设备和 API 的渲染和优化。

相关新闻和技术文章

相关视频

© . This site is unofficial and not affiliated with AMD.