🦉一文理解 FP16 Shader
00 分钟
2024-9-13
2024-9-13
type
status
date
slug
summary
tags
category
icon
password

前言

最近 EA 在 GDC 2024 上分享了一篇文章,叫做 FP16 in Frostbite(资料见附录),正好结合自己之前做 FP16 优化的一些经验,写一篇文章做做分享。
 
FP16 全称 Float 16,按字面意思理解就是位宽为 16 Bit 的浮点类型,与之相对应的是传统的 32 位浮点类型,也即 FP32 / Float 32。由于 GPU 计算压力需求的激增,FP16 在近些年来已经成为了一个越来越常见的优化手段,尤其是移动和主机平台。但是 FP16 从来也不是一个 Free To Use 的特性,使用它也是有代价的。
 
想要在工程里用 FP16 做优化获取性能收益要付出的代价是非常大的,往往是避免不了大量的反复修改对照,还要开发相关的辅助工具,比较佩服 EA 的毅力。

FP16 的优劣

FP16 相比 FP32,指数位和底数位所占 Bit 都有所减少,这就意味着 FP16 相比 FP32 精度要大打折扣,但只要精度还够用,FP16 相比 FP32 会有很多优势。
notion image
最重要的一个优势就是在现代 GPU 里,原本存放一个 FP32 的寄存器里能放下两个 FP16 数据,这就可以有效减少寄存器开销。我们知道现代 GPU 如果出现寄存器不够用的情况下,会减少线程组里派发的线程,从而降低 Shader 的并发性,在一些情况下,一旦寄存器占用越过某个阈值,Shader 性能会成倍下降。所以说 FP16 是优化寄存器占用的利器。
notion image
类似的,现代 GPU 的一个 FP32 的 ALU 也可以同时处理两个 FP16 数据,这就意味着如果你能把多个要执行相同计算的 FP32 数据改成 FP16,就可以成倍地降低 ALU 的开销。
notion image
最后,在一些有专用 FP16 指令的硬件上,对一个 FP16 和 FP32 执行同样的计算,FP16 版本的指令会更加省电。
notion image
听起来好像很美好,但是别忘了 FP16 最大的硬伤是精度问题,很多时候如果你拿 FP16 类型去保存了需要高浮点数精度的数据,那没啥好说的,就是直接计算错误或者渲染错误。例如存大图的 UV:
notion image
另外要注意的一点是虽然 FP16 虽然可以 Pack 到一个 FP32 寄存器里,但是很多时候没办法凑齐俩 FP16 进行 Pack,这时候直接填入 FP32 寄存器反而会带来两次数据转换指令的开销,因为填入寄存器要先从 FP16 转成 FP32,移出寄存器又要从 FP32 转回 FP16,导致性能反而变差,这点在使用 FP16 的时候一定要注意,所以改 FP16 要遵循一个原则 —— “能改多改,少改不如不改”。

兼容性

FP16 并不是所有 GPU 都支持的,当然现在支持的 GPU 已经越来越多了,按照平台来看支持情况如下:
移动平台
绝大部分硬件都支持 FP16,但是因为硬件实现不一致可能存在精度差异。
主机
PS5/Xbox/Switch 最新时代机器都支持 FP16。
PC
支持 SM6.2 的硬件要求支持 FP16,以下的设备可能存在不支持和部分支持的情况。
其中移动平台因为独特的 TBDR 架构,相对通过 FP16 获取收益会比较简单,主机和 PC 获取收益相对较难(或许驱动有很大原因,也可能是 IMR 架构下 FP16 显得没那么重要)。
 
另外我们要知道,HLSL 里其实是支持两种 FP16 类型的:
float16_t
真正在任何地方都是占 16 Bit 的浮点数类型。要求编译 HLSL 时添加 —enable-16bit-types 参数,并且 HLSL 版本要 ≥ 2018,还需要硬件支持。
min16float
数据存储还是 32 Bit 浮点数,在填入寄存器的时候可能会被编译器优化成 16 Bit 对待,要求硬件支持才能生效,不过就算不支持也不至于跑不起来。
要注意的是因为 min16float 在存储上还是 32 Bit,在 Uniform Buffer 或者 Storage Buffer 中使用这个类型作为定义时,在 C++ 里喂给 Shader 的数据还是要 32 Bits 的,而 float16_t 则是从 C++ 开始就得一路使用 16 Bits 来存储。
 
由于抽象的平台兼容性以及 HLSL 自己的精神分裂,想要在所有平台上支持 FP16 Shader,最重要的一件事就是先对平台进行分类,我们可以参考下 EA Frostbite 的分类:
notion image
Full FP16 真正使用 float16_t,在 C++ 喂 Uniform Buffer 也是喂 16 Bits 数据,Relaxed FP16 使用 min16float,Uniform Buffer 喂 32 Bits 数据,Remapped FP16 直接把 FP16 重定义成 FP32 使用来保证兼容性。

工程实践

Frostbite 为了能让一套 Shader 兼容所有平台,首先就要定义一组类型,然后在不同的平台上转译成不同的真正类型:
notion image
因为三个平台所需要的 Uniform Buffer 数据有 16 位和 32 位版本,C++ 这边也要做相应的处理,Frostbite 是直接在基类模板特化里去定义具体类型,然后子类里面直接用,也可以参考下:
notion image
notion image
然后只需要根据平台选用不同的 Constant Buffer 即可:
notion image
有了这些基础设施之后,其实 FP16 优化就会变成一个相当重复的体力劳动,无非就是先找出来哪些变量或许可以改,然后改掉看看渲染效果或者计算结果会不会有错误,最后测试性能收益。
 
Frostbite 在工程实践中其实做了很多工具相关的优化,其中最重要的一个工具就是即使切换 FP16 / FP32 去对比差异:
notion image
这一点其实相当重要,要是没有这个工具基本你很难知道自己改的 FP16 会对渲染结果产生多大影响,久而久之影响渲染质量最后所谓的优化变成降低画面质量来提升性能。
 
另外 Frostbite 还有一个自动化渲染测试工具,可以自动跟之前存档的渲染图片做对比,快速发现修改带来的渲染问题:
notion image
基本做 FP16 优化也是跑不掉做这个工具,它能保证你的优化不是牺牲画面质量换取性能。
 
最后可以看看 Frostbite 给出的一个 Shader 的测试数据,也比较有意思:
notion image
基本上跟我之前做 FP16 优化的经验是一样的,FP16 在移动端获取收益相当简单,而且你基本上必做,因为移动端不光有性能优化需求,降低耗电也至关重要,FP16 和 FP32 版本 Shader 的耗电还是差别不小的。而主机、PC 上虽然 FP16 已经有不少设备完全支持了,但是就算做了很深入的优化,还是收益不大,甚至是负收益,或许是因为 PC 的硬件架构强大到 FP16 和 FP32 指令差别不大,而且 FP16 数据如果 Pack 不当,会导致插入很多 FP16 / FP32 转换的指令降低性能。
 
总结一下就是如果你的引擎需要支持移动端,那么 FP16 的支持毋庸置疑,必做。如果你的引擎只支持 PC 和主机,那么可以再观望下,短期内看优化必要也不大,但是未来随着硬件升级,或许 FP16 能带来收益,有闲工夫的话支持下也未尝不可,未来直接无缝衔接。

附录

上一篇
利用 C++ Concepts 做编译期检查
下一篇
一文理解超分辨率

评论
Loading...