低精度算子单测开发规范¶
一、FP16 单测添加¶
Step1:确定任务情况¶
任务明细表见附录
寻找任务
在任务总表中查看‘实际开发者’字段,寻找到自己的算子/单测开发任务
算子对应单侧文件位置
算子对应的单测在目录test/legacy_test下,每个算子对应的单测文件可参考任务总表中的 ‘单测文件’ 字段
算子对应类别
算子所属类别可以参考 ‘分类’ 字段
判断算子需要添加还是完善单测
算子需要完成的任务在 ‘任务统计’ 字段中给出,主要分为 2 个类别:
‘增加 FP16 支持’字段为‘是’。 需要添加 FP16 算子支持(参考低精度算子支持开发规范),同时需要添加 FP16 的单测支持,单测添加过程参考 Step2。
‘增加 FP16 支持’字段为空,‘完善 FP16 单测’字段为‘是’ 。需要完善 FP16 单测,单测相应问题在 ‘单测添加所需注意事项或存在问题’ 中给出,主要分为两类
补充单测。参考 Step2。
修改阈值设置。参考 Step2 的 3、4。
Step2:具体单测添加步骤¶
单测添加原则:
若有 FP32 类型的 OpTest,则 FP16 的 OpTest 测例数量需与之对应
若无 FP32 类型 OpTest,只有 API Test,则需添加 FP32 及 FP16 的 OpTest,添加过程见 2,添加测例数量与 API Test 对应
1、确定单测类名和单测的基类¶
对于添加 FP16 单测
如果原单测文件内部含有 OpTest,则添加继承 OpTest 的 FP16 的单测。建议采用“Test” + Op_name + “FP16OP”作为 FP16 单测类名。
如果原单测文件内部仅含有 APITest,则需添加 FP32 和 FP16 的 OpTest,命名规则与上述相同。
对于完善 FP16 单测,原先的类名不需要修改。
2、添加/修改 OpTest¶
2.1 修改 setUp 方法¶
定义完类的头部后,则对内部的方法进行修改。
对于添加 FP32/FP16 单测,参考以下步骤完成 setUp 函数的添加。
主要有以下几个修改:
a) 修改 self.op_type。 如代码 1-1 的第 5 行。 对于要设置的值可以参考同一个单测文件内部已有的单测类的 self.op_type 情况。
b) 修改 self.dtype。设置为 np.float16。如代码 1-1 的第 7 行。
c) 修改输入 self.inputs 和输出 self.outputs。
数据生成。如代码 1-1 的第 9 行所示。
需要生成与 self.dtype 类型相同的数据。可使用 astype(self.dtype)完成
生成时通常采用 numpy.random 包。多数情况下使用 numpy.random.random 或 numpy.random.uniform 函数。
具体的函数、shape 形状、参考同文件下的其他单测的设置。
设置 self.inputs。如代码 1-1 的第 15 行所示。
inputs 部分需要上述生成的输入数据。
设置 self.outputs。如代码 1-1 的第 17 行所示。
首先需要对输入数据进行计算,对于复杂一些的计算,可能会使得 setUp 函数过分冗长,可以写成额外的函数, 如代码 1-1 的第 13 行 。
outpus 部分需要传入由 numpy 计算出的参考结果。
代码 1-1
class TestAFP16OP(OpTest):
#...
def setUp(self):
#self.op_type 用来指定当前 OP 的类型,可参考同单测文件下的 op_type
self.op_type = 'A'
#dtype 需要设置为 np.float16 形式
self.dtype = np.float16
#生成初始输入数据 x,通常使用 numpy.random 包
x = np.random.rand(2,3,5).astype(np.float32)
#计算输出数据 out
#复杂的计算可以自己编写函数完成计算
#简单的计算可以直接在 setUp 中计算,如加法等
out,... = self.compute_output(x,...)
#inputs 需要传入 FP16 类型的数据,用作单测的输入数据
self.inputs = {'X': x.astype(self.dtype)}
#outputs 需要传入 FP32 类型的数据,用作单测的参考数据
self.outputs = {'Out': out}
#op_type = 'sequence_reshape'的单测中的 compute_coutput
def compute_output(self, x, x_lod, dimension):
x_width = x.shape[1]
out_lod = [[]]
for i in range(len(x_lod[0])):
seq_len = x_lod[0][i]
offset = (seq_len * x_width) / dimension
assert int(offset) * dimension == seq_len * x_width
out_lod[0].append(int(offset))
out = np.zeros(shape=(sum(out_lod[0]), dimension)).astype('float64')
out.ravel()[:] = x.ravel()[:]
return out, out_lod
2.2 修改 test_check_output 方法¶
test_check_output 中添加对 check_output 的调用,如代码 1-2 所示。
在 check_output_with_place 调用时,按照各分类传入建议的绝对误差阈值 atol。
绝对误差阈值应当按照输出结果根据公式估算
$$ (E = 2^{\lfloor\log_{2}^{E_{out}}\rfloor-10})(公式 1-1) $$
绝大多数单测生成的数据和结果在[0, 2)之间,所以推荐各类别可使用阈值建议如下,如果存在核验不通过可通过公式 1-1 按照输出结果的最大取值计算推荐绝对误差。
不涉及计算。设置阈值 1e-3。
基本运算算子。设置阈值 1e-3。
基本数学函数,主要为激活函数。设置阈值 1e-3。
累加函数。
对于 $E_{out}$, 可以按照算子计算公式、输入数据的数学期望、输入数据个数来进行估计。
i.纯累加。可以用如下估算误差值。
$$ E_{out}=E_{in} * N = \frac{max + min}{2} * N ,[min, max) 随机生成方式 $$
ii.取平均。可以用如下估算误差值。
$$ E_{out}=E_{in} = \frac{max + min}{2},[min, max) 随机生成方式 $$
iii.Softmax 类。设置阈值 1e-3
iv.interp 类。设置阈值 1e-3
v.norm 类。设置阈值 1e-3
vi.matmul。设置阈值 1e-3
示例:
我们在代码 1-3 中以 reduce_sum 为例。
在 setUp 中,我们生成了 1000 大小的累加序列。采用的是[0, 0.1)的均匀分布,数学期望 $E_{in}=0.05$
对于 1000 个数的累加结果的数学期望为 $E_{out}=E_{in}*1000=50$
那我们的估算绝对误差为 $E=2^{\lfloor\log_{2}^{E_{out}}\rfloor-10}=0.03125$
在 check_output 时通过 atol 参数设置该误差
代码 1-2
class TestAFP16OP(OpTest):
#...
def test_check_output(self):
if core.is_compiled_with_cuda():
place = core.CUDAPlace(0)
if core.is_float16_supported(place):
#使用 atol 指定前向计算的绝对误差阈值
self.check_output_with_place(place, atol=1e-3)
代码 1-3
class TestSumOpFP16(OpTest):
#...
def setUp(self):
#...
x = np.random.uniform(0, 0.1, (1000)).astype('float32')
#...
def test_check_output(self):
self.check_output(check_eager=True, atol=0.03125)
2.3 修改 test_check_grad 方法¶
test_check_grad 中添加对 check_grad 的调用,如代码 1-4 所示。
在 check_grad_with_place 调用时,按照各分类传入建议的相对误差阈值 max_relative_error。
各类别处理:
不涉及计算。设置阈值 1e-3。
基本运算算子。设置阈值 1e-3。
基本数学函数,主要为激活函数。设置阈值 1e-2, 对于 exp, expm1,tan,cosh,sinh,reciprocal,square, Stanh 采用阈值 0.1。
累加函数。设置阈值 5e-3。
norm。设置阈值 5e-3
softmax。设置阈值 1e-3
interp。设置阈值 1e-2
matmul。设置阈值 5e-3
cumsum。设置阈值 1e-2。
logsum。设置阈值 1e-2。
logcumsumexp。设置阈值 0.5。
代码 1-4
def TestAFP16OP(OpTest):
#...
def test_check_grad(self):
if core.is_compiled_with_cuda():
place = core.CUDAPlace(0)
if core.is_float16_supported(place):
#使用 max_relative_error 指定反响的相对误差阈值
self.check_grad_with_place(place, ['X'], 'Out', max_relative_error=1e-2)
二、BF16 单测添加¶
Step1:确定任务情况¶
寻找任务
a. 在任务总表中查看‘实际开发者’字段,寻找到自己的算子/单测开发任务
算子对应单侧文件位置
a. 算子对应的单测在目录test/legacy_test下,每个算子对应的单测文件可参考任务总表中的 “单测文件” 字段
算子对应类别
a. 算子所属类别可以参考 ‘分类’ 字段,按照 Step2 部分的指引添加相应的单测
判断算子需要添加还是完善单测
a. 算子需要完成的任务在 ‘任务统计’ 字段中给出,主要分为 2 个类别:
‘增加 BF16 支持’ 字段为 ‘是’ 。需要添加 BF16 算子支持(参考算子添加规范),同时需要添加 BF16 的单测支持,单测添加过程参考 Step2。
‘增加 BF16 支持’ 字段为 空 , ‘完善 BF16 单测’ 字段为‘是’ 。需要完善 BF16 单测,单测相应问题在 ‘单测添加所需注意事项或存在问题’ 中给出,主要分为两类
补充单测
修改阈值设置
Step2:具体单测添加步骤¶
1. 确定单测类名和单测的基类¶
对于添加 BF16 单测,建议是 “Test” + Op_name + “BF16”作为 BF16 单测类名,将 OpTest 作为基类。如代码 2-1 的第 1 行。
2. 修改 setUp 方法¶
setUp 中需要完成数据生成,添加输入和输出数据。setUp 需要设置的内容,参考同一个单测文件下已有的单测类。但主要有以下几点。
首先,修改 self.op_type。 如代码 2-1 的第 5 行。 对于要设置的值可以参考同一个单测文件内部已有的单测类的 self.op_type 情况。
其次,修改 self.dtype。设置为 np.uint16。如代码 2-1 的第 7 行。
最后,修改输入 self.inputs 和输出 self.outputs。
BF16 在传入输入和输入参考值时需要调用convert_float_to_uint16方法。
数据生成。 如代码 2-1 的第 9 行所示 。
需要生成 FP32 类型数据。可使用 astype(np.float32)转换成 FP32 格式
生成时通常采用 numpy.random 包。利用numpy.random.random或 numpy.random.uniform 。
具体的函数、shape 形状、参考同文件下的其他单测的设置。
设置 self.inputs。如代码 2-1 的第 13 行所示。
inputs 部分需要传入 Uint16 格式的数据。可使用convert_float_to_uint16完成转换。
设置 self.outputs。如代码 2-1 的第 15 行所示。
outpus 部分需要传入 Uint16 格式的参考结果。可使用convert_float_to_uint16完成转换。
代码 2-1
def TestABF16(OpTest):
#...
def setUp(self):
#self.op_type 用来指定当前 OP 的类型,可参考同单测文件下的 op_type
self.op_type = 'A'
#dtype 需要设置为 np.uint16 形式
self.dtype = np.uint16
#生成初始输入数据 x,通常使用 numpy.random 包
x = np.random.rand(2,3,5).astype(np.float32)
#计算输出数据 out
#复杂的计算可以自己编写函数完成计算
#简单的计算可以直接在 setUp 中计算,如加法等
out = compute_out(x)
#inputs 需要传入 uint16 类型的数据,使用 convert_float_to_uint16 来获得
self.inputs = {'X': convert_float_to_uint16(x)}
#outputs 需要传入 uint16 类型的数据,使用 convert_float_to_uint16 来获得
self.outputs = {'Out': convert_float_to_uint16(out)}
3. 修改 test_check_output 方法¶
test_check_output 中添加对 check_output 的调用,如代码 2-2 所示。
在 check_output_with_place 调用时,按照各分类传入建议的绝对误差阈值 atol。
绝对误差阈值应当按照输出结果根据公式估算
$$ E = 2^{\lfloor\log_{2}^{E_{out}}\rfloor-8}(公式 2-1) $$
绝大多数单测生成的数据和结果在[0, 2)之间,所以推荐各类别可使用阈值建议如下,如果存在核验不通过可通过公式 1-1 按照输出结果的最大取值计算推荐绝对误差。
不涉及计算。设置阈值 1e-2。
基本运算算子。设置阈值 1e-2。
基本数学函数,主要为激活函数。设置阈值 1e-2。
累加函数。
i.纯累加。可以用如下估算误差值。
$$ E_{out}=E_{in} * N = \frac{max + min}{2} * N ,[min, max) 随机生成方式 $$
ii.取平均。可以用如下估算误差值。
$$ E_{out}=E_{in} = \frac{max + min}{2},[min, max) 随机生成方式 $$
iii.Softmax 类。设置阈值 1e-2
iv.interp 类。设置阈值 1e-2
v.norm 类。设置阈值 1e-2
vi.matmul。设置阈值 1e-2
代码 2-2
def TestABF16(OpTest):
#...
def test_check_output(self):
self.check_output(atol=1e-2)
4. 修改 test_check_grad 方法¶
test_check_grad 中添加对 check_grad 的调用,如代码 2-3 所示。
各类别处理:
不涉及计算。设置阈值 1e-2。
基本运算算子。设置阈值 1e-2。
基本数学函数,主要为激活函数。设置阈值 1e-2。
累加函数。设置阈值 1e-2。
代码 2-3
def TestABF16(OpTest):
#...
def test_check_grad(self):
self.check_grad(['X'], 'Out', max_relative_error=1e-2)
三、验证单测添加是否正确¶
3.1 本地环境验证¶
建议优先通过本地环境进行调试。
编译时,请打开测试选项 -DWITH_TESTING=ON ,并使用 make -j$(nproc)完成 Paddle 编译
cmake .. -DWITH_GPU=ON -DWITH_TESTING=ON make -j $(nproc)
pip 安装编译好的 whl 包,位于 build/python/dist 下。
pip install python/dist/paddlepaddle-0.0.0-cpXX-cpXXm-linux_x86_64.whl
运行单测,验证是否通过。
# 指定单测 make test ARGS="-R test_mul_op -V" # test_xxx 是单测文件名称 或 ctest -R test_mul_op [-V] #-V 可以打印详细的测试信息
如果通过,则添加正确;如果没有通过,请根据报错信息完成修改。
3.2 CI 验证¶
提交 PR 以后,CI 会对本次修改进行检查,出错的单测将被报出,可在全量日志中这次报错是否与本次修改有关
3.3 特定 CI 的 Approve¶
对于单测精度阈值(atol, max_relative_error 等)的修改会出触发 CI-Approval,请根据 CI 报错的指引,请对应的 RD review 并 approve 这个 PR
四、常见问题总结¶
1. op_type、op_name 和测试文件对应关系¶
op_type(在 OpTest 中调用的名字) | op_name(Paddle 中注册的名字) | 测试文件 |
---|---|---|
sum | add_n | test_sum_op.py |
gaussian_random | gaussian | test_gaussian_random_op.py |
elementiwise_add/sub/mul/div/max/min | add/substract/multiply/divide/maximum/minimum | test_elementiwise_add/sub/mul/div/max/min_op.py |
argmax/argmin | argmax/argmin | test_arg_min_max_op.py |
bilinear_interp | bilinear | test_bilinear_interp_op.py |
bicubic_interp | bicubic | test_bicubic_interp_op.py |
check_finite_and_unscale | check_finite_and_unscale | test_amp_check_finite_and_scale_op.py |
softmax_with_cross_entropy | cross_entropy_with_softmax | test_softmax_with_cross_entropy_op.py |
depthwise_conv2d | depthwise_conv2d | test_conv2d_op_depthwise_conv.py |
gaussian | gaussian_random | test_gaussian_random_op.py |
less_than/less_equal/greater_than/greater_equal/equal/not_equal | less_than/less_equal/greater_than/greater_equal/equal/not_equal | test_compare_op.py |
isinf/isnan | isinf/isnan | test_isfinite_op.py |
matmul_v2 | matmul | test_matmul_v2_op.py |
segment_pool | segment_pool | test_segment_ops.py |
tril、triu | tril_triu | test_tril_triu_op.py |
uniform_random | uniform | test_uniform_random_op.py |
fused_softmax_mask | fused_softmax_mask | test_softmax_mask_fuse_op.py |
p_norm | p_norm | test_norm_all.py |
reduce_sum | sum | test_reduce_op.py |
fill | fill_any | test_fill_any_op.py |
range | arange | test_arange.op |
nonzero | nonzero | test_nonzero_api.py |
unique | unique | test_unique.py |
linspace | linspace | test_linspace.py |