1、 Paddle 算子实现float16据类型扩展
该项目为 【PaddlePaddle Hackathon 3】数据类型扩展任务合集中任务No.47和No.48开发过程分享
2、任务介绍
-
为 Paddle logsumexp 算子实现 float16 数据类型支持
由于 sum 类运算直接采用 float16 累加会有溢出风险,因此该功能要求为 logsumexp 算子注册 float16 类型,同时改写算子 Kernel,使得 float16 下精度与期望结果的误差不超过 1e-3,并且性能不差于使用 float32 类型计算。 -
为 Paddle elementwise 系列算子实现 float16 数据类型支持
elementwise 类的部分算子未支持 float16 类型,因此该功能要求为这类算子注册 float16 类型,同时改写算子实现,使得 float16 下精度与期望结果的误差不超过 1e-3,并且性能不差于使用 float32 类型计算。
任务1:需注册 float16 类型,并对算子实现做简单改写 elementwise_pow、elementwise_pow_grad、elementwise_pow_raw、elementwise_mod;
任务2:只需注册 float16 类型的有 elementwise_fmax、elementwise_fmax_grad、elementwise_fmin、elementwise_fmin_grad、elementwise_heaviside_raw、elementwise_heaviside。
3、设计文档
初次参与paddle算子开发,对各个模块还不熟悉的情况下,可以大胆摸着石头过河。
增加算子数据类型支持float16任务并不涉及算子数学公式。开发的难度主要来自于对paddle算子框架和对c++等各种基础数学运算库支持的数据类型的理解。
-
算子数据类型float16注册。
首先在logsumexp的前向和后向算子注册中添加新增支持的数据类型float16。这一步设计没什么可说的,只需要简单的在代码入口处添加一次类型注册。但这一步对于整个算子数据类型开发的重要基石步骤。对于paddle算子入门来说,摸着石头过河就是不断试错过程。注册算子就是第一步摸石头。
实际上我们也不知道注册完之后数据类型float16接下来该去开发处理哪些方法,会不会有问题。我们在注册完类型之后,就直接去编译,然后尝试测试运行和对比精度,当中遇到什么问题解决什么问题,即所谓逢山开路遇水搭桥。
当遇到编译问题,就说明这个算子数学运算原始实现中并不完全兼容float16,需要自己去手动更改代码完成兼容。如果幸运编译过了,那就有可能这个算子的实现设计原本就能自动支持float16类型扩展,这时我需要再进一步通过测试去看该算子在float16下是否能完成整个运算过程以及最终的精度是否达标。如果测试中遇到问题,那我们还是需要去考虑原始算子的实现对于新类型float16兼容有哪些地方需要单独处理。
简单来说注册完新增支持的数据类型后,当能够编译运行并且结果精度都没问题,那说明这个算子直接完成一次类型注册就可以了,我们甚至都可以不用去看具体的算子实现了,该算子的开发任务也算直接简单的就完成了,如不能运行,我们需要针对具体算子更改算子设计实现。 -
针对具体算子更改其算子设计和实现以兼容float16数据类型。
由于各个算子的涉及的数学运算各不相同,在实现过程中需要考虑对应算子的计算步骤中涉及的精度损失,溢出风险和数学运算库的数据类型兼容性,如std math库和Eigen张量计算库。
在设计算子实现中需要注意的事项有:
<1>当算子实现中存在诸如exp、log、sum指数、对数和累加运算,需要使用float作为计算类型以防止运算溢出。
<2>算子原始的代码设计多是基于模板的,而float16可能并不兼容原始代码的各个运算步骤,一个比较方便的方式是在原本模板上简单修改,比如处理上述的溢出风险,我们需要在算数运算前对数据cast到float再运算。有些算子可能比较复杂,在原始实现上反复增加额外开支以兼容flaot16可能会影响其他数据类型的性能,此时一个比较好的处理方式是对新增支持的数据类型做模板特例化,单独处理float16类型算子的计算过程,以避免影响算子性能。同时,特例化还能单独处理并避免由于std算数运算库对于dtype::float16不完全兼容而出现的模板参数类型推导中出现的隐式类型推导歧义,如图:<3>所有改动都需要考虑在不影响算子其他数据类型性能的基础上进行,尽量精简代码,增加的额外开销越少越好。在原始算子流程中兼容新数据类型时,推荐使用paddle定义的phi::dtype::MPTypeTrait::Type对运算的兼容数据类型做自动推导,以避免影响其他数据类型的性能和运算结果。
-
测试代码设计。
测试代码设计也是要尽可能的利用已有的测试流程。如果只增加注册一个新数据类型测试就能顺利完成测试那就沿用其他数据类型测试的数据和过程,我们就不需要为该测试额外增加新的数据用例。只有部分需要特殊处理才能完成测试。
例如logsumexp算子,原本的测试是兼容float及以上精度的的数据类型并采用数值方式进行的梯度求解,当测试针对float16时,作为对比数据的梯度运算采用数值运算求解的结果精度损失较大,无法胜任算子精度测试,需要进行单独求梯度。
除了添加单测,性能测试代码需要根据算子用途每一个算子单独设计。 -
为中文和英文文档添加算子已支持新数据类型的注释说明。
英文文档在python/paddle/tensor/math.py。
中文文档在https://github.com/PaddlePaddle/docs。
4、代码开发
-
算子数据类型float16注册。
一般每个算子的新增数据类型注册需要添加两处,一处是注册给当前算子函数,一处是注册给当前算子的梯度函数。
以logsumex为例,需要在logsumexp_kernel.cu和logsumexp_grad_kernel.cu中添加logsumexp的float16注册。
- 针对具体算子更改其算子设计和实现以兼容float16数据类型。
同样以logsumex算子的实现为例,算子实现和算子梯度实现在logsumexp_kernel_impl.h和logsumexp_grad_kernel_impl.h。
- 测试代码设计。
同样以logsumex算子新增数据类型float16数据类型为例,新增测试代码在python/paddle/fluid/tests/unittests/test_logsumexp.py。
该单测中,logsumex算子采用低精度的float16难免有一些精度损失,我们在满足精度要求的情况下可以重写下test_check_output和test_check_grad函数,并且指定大一些的atol、max_relative_error精度阈值,同时由于数值方式计算的对比梯度精度也存在偏差,参照梯度需要自行计算。顺便原始单测中漏掉了float类型的测试也在本次单测中一并添加。
性能测试代码根据算子用途单独编写。如下是elementwise 系列算子中fmin性能测试代码示例:
感谢 paddle 的大佬们对我的代码反复耐心的review,帮我找到了代码中的错误,协助商讨修改方案。在大家的帮助下,最终完成了本次参赛任务。(PR 链接和PR 链接)。
5、运行测试及性能测试
性能测试可以使用PaddlePaddle官方仓库的性能测试工具,也可以采用nvprof,nvprof是用来测试了解并优化CUDA或OpenACC应用程序的性能的分析工具。分析工具使您能够从命令行收集和查看分析数据,例如nvprof python test_pow.py,我们采用了nvprof直接测量核函数运行时间的变化。
logsumexp 算子优化前后性能对比:
Case No. | input_shape | FP32 Perf(us) | FP16 Perf(us) | diff |
---|---|---|---|---|
0 | [1000, 130, 17] | 173.735 | 206.377 | 0.842 |
1 | [1000, 100, 10, 10] | 576.93 | 651.485 | 0.886 |
2 | [1000, 100, 200] | 1089.02 | 1219.52 | 0.893 |
3 | [100, 1000, 25, 40] | 5195 | 5766.75 | 0.901 |
4 | [100, 1000, 250, 40] | 40763 | 39548.1 | 1.031 |
5 | [100, 1000, 250, 50] | 64426 | 50173.5 | 1.284 |
(diff = FP32 / FP16) | ||||
当数据规模逐渐增大的时候,FP16的性能逐渐优于FP32,当小规模数据时两者性能差距不大。 |
elementwise 系列算子优化前后性能对比:
Case No. | input_shape | FP32 Perf(us) | FP16 Perf(us) | diff |
---|---|---|---|---|
0 pow | [1000, 130, 17] | 109.41 | 104.38 | 1.048 |
1 pow | [1000, 100, 10, 10] | 474.33 | 432.76 | 1.096 |
2 pow | [1000, 100, 200] | 937.03 | 854.88 | 1.096 |
3 pow | [100, 1000, 25, 40] | 4456.1 | 3663.3 | 1.216 |
0 mod | [1000, 130, 17] | 95.715 | 69.184 | 1.383 |
1 mod | [1000, 100, 10, 10] | 472.66 | 295.51 | 1.599 |
2 mod | [1000, 100, 200] | 963.66 | 578.45 | 1.665 |
3 mod | [100, 1000, 25, 40] | 4432.7 | 2456.8 | 1.804 |
0 fmin | [1000, 130, 17] | 95.847 | 55.266 | 1.734 |
1 fmin | [1000, 100, 10, 10] | 471.40 | 221.61 | 2.127 |
2 fmin | [1000, 100, 200] | 965.76 | 434.67 | 2.221 |
3 fmin | [100, 1000, 25, 40] | 4393.4 | 1964.7 | 2.236 |
0 fmax | [1000, 130, 17] | 97.508 | 52.384 | 1.861 |
1 fmax | [1000, 100, 10, 10] | 478.41 | 219.75 | 2.177 |
2 fmax | [1000, 100, 200] | 938.72 | 430.82 | 2.178 |
3 fmax | [100, 1000, 25, 40] | 4443.8 | 1975.2 | 2.249 |
0 heaviside | [1000, 130, 17] | 95.455 | 51.394 | 1.857 |
1 heaviside | [1000, 100, 10, 10] | 481.63 | 221.41 | 2.175 |
2 heaviside | [1000, 100, 200] | 952.66 | 434.75 | 2.191 |
3 heaviside | [100, 1000, 25, 40] | 4438.8 | 2003.0 | 2.216 |
(diff = FP32 / FP16) | ||||
elementwise 系列算子优化后FP16的性能整体优于FP32。 |
6、总结
该任务是第三期 paddle Hackathon 其中一项数据类型扩展任务,算子数据类型扩展是针对飞桨已有的算子,本次任务目标是为 Paddle logsumexp 算子实现 float16 数据类型支持和 Paddle elementwise 系列算子实现 float16 数据类型支持。这两个任务是paddle Hackathon一星难度的基础任务总体上是比较容易上手的,作者是才刚刚接触paddle开源项目,在业余时间持续了不到一个月大概逐渐上手熟悉本paddle项目,进而着手开展paddle Hackathon算子数据类型扩展任务。相信所有参与到paddle开发中的小伙伴都能从中找到学习到新知识的乐趣。
希望能利用我的开发经历,来给参与到paddle项目中的朋友们分享经验,为 PaddlePaddle 锦上添花,通过paddle一起成长。
最后也衷心感谢各位在paddle开发中帮助我的朋友们。
此文章为搬运
原项目链接