低精度算子单测开发规范

一、FP16 单测添加

Step1:确定任务情况

任务明细表见附录

  1. 寻找任务

    在任务总表中查看‘实际开发者’字段,寻找到自己的算子/单测开发任务

  2. 算子对应单侧文件位置

    算子对应的单测在目录test/legacy_test下,每个算子对应的单测文件可参考任务总表中的 ‘单测文件’ 字段

  3. 算子对应类别

    算子所属类别可以参考 ‘分类’ 字段

  4. 判断算子需要添加还是完善单测

    算子需要完成的任务在 ‘任务统计’ 字段中给出,主要分为 2 个类别:

    1. 增加 FP16 支持’字段为‘’。 需要添加 FP16 算子支持(参考低精度算子支持开发规范),同时需要添加 FP16 的单测支持,单测添加过程参考 Step2。

    2. 增加 FP16 支持’字段为空,‘完善 FP16 单测’字段为‘’ 。需要完善 FP16 单测,单测相应问题在 ‘单测添加所需注意事项或存在问题’ 中给出,主要分为两类

      1. 补充单测。参考 Step2。

      2. 修改阈值设置。参考 Step2 的 3、4。

Step2:具体单测添加步骤

单测添加原则

  1. 若有 FP32 类型的 OpTest,则 FP16 的 OpTest 测例数量需与之对应

  2. 若无 FP32 类型 OpTest,只有 API Test,则需添加 FP32 及 FP16 的 OpTest,添加过程见 2,添加测例数量与 API Test 对应

1、确定单测类名和单测的基类

  1. 对于添加 FP16 单测

    1. 如果原单测文件内部含有 OpTest,则添加继承 OpTest 的 FP16 的单测。建议采用“Test” + Op_name + “FP16OP”作为 FP16 单测类名。

    2. 如果原单测文件内部仅含有 APITest,则需添加 FP32 和 FP16 的 OpTest,命名规则与上述相同。

  2. 对于完善 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-1 的第 9 行所示。

    需要生成与 self.dtype 类型相同的数据。可使用 astype(self.dtype)完成

    生成时通常采用 numpy.random 包。多数情况下使用 numpy.random.random 或 numpy.random.uniform 函数。

    具体的函数、shape 形状、参考同文件下的其他单测的设置。

  2. 设置 self.inputs。如代码 1-1 的第 15 行所示。

    inputs 部分需要上述生成的输入数据。

  3. 设置 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 按照输出结果的最大取值计算推荐绝对误差。

  1. 不涉及计算。设置阈值 1e-3。

  2. 基本运算算子。设置阈值 1e-3。

  3. 基本数学函数,主要为激活函数。设置阈值 1e-3。

  4. 累加函数。

对于 $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。

各类别处理:

  1. 不涉及计算。设置阈值 1e-3。

  2. 基本运算算子。设置阈值 1e-3。

  3. 基本数学函数,主要为激活函数。设置阈值 1e-2, 对于 exp, expm1,tan,cosh,sinh,reciprocal,square, Stanh 采用阈值 0.1。

  4. 累加函数。设置阈值 5e-3。

    1. norm。设置阈值 5e-3

    2. softmax。设置阈值 1e-3

    3. interp。设置阈值 1e-2

    4. matmul。设置阈值 5e-3

    5. cumsum。设置阈值 1e-2。

    6. logsum。设置阈值 1e-2。

    7. 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:确定任务情况

  1. 寻找任务

    a. 在任务总表中查看‘实际开发者’字段,寻找到自己的算子/单测开发任务

  2. 算子对应单侧文件位置

    a. 算子对应的单测在目录test/legacy_test下,每个算子对应的单测文件可参考任务总表中的 “单测文件” 字段

  3. 算子对应类别

    a. 算子所属类别可以参考 ‘分类’ 字段,按照 Step2 部分的指引添加相应的单测

  4. 判断算子需要添加还是完善单测

    a. 算子需要完成的任务在 ‘任务统计’ 字段中给出,主要分为 2 个类别:

    1. ‘增加 BF16 支持’ 字段为 ‘是’ 。需要添加 BF16 算子支持(参考算子添加规范),同时需要添加 BF16 的单测支持,单测添加过程参考 Step2。

    2. ‘增加 BF16 支持’ 字段为 ‘完善 BF16 单测’ 字段为‘’ 。需要完善 BF16 单测,单测相应问题在 ‘单测添加所需注意事项或存在问题’ 中给出,主要分为两类

      1. 补充单测

      2. 修改阈值设置

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方法。

  1. 数据生成。 如代码 2-1 的第 9 行所示

    需要生成 FP32 类型数据。可使用 astype(np.float32)转换成 FP32 格式

    生成时通常采用 numpy.random 包。利用numpy.random.randomnumpy.random.uniform

    具体的函数、shape 形状、参考同文件下的其他单测的设置。

  2. 设置 self.inputs。如代码 2-1 的第 13 行所示。

    inputs 部分需要传入 Uint16 格式的数据。可使用convert_float_to_uint16完成转换。

  3. 设置 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 按照输出结果的最大取值计算推荐绝对误差。

  1. 不涉及计算。设置阈值 1e-2。

  2. 基本运算算子。设置阈值 1e-2。

  3. 基本数学函数,主要为激活函数。设置阈值 1e-2。

  4. 累加函数。

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 所示。

各类别处理:

  1. 不涉及计算。设置阈值 1e-2。

  2. 基本运算算子。设置阈值 1e-2。

  3. 基本数学函数,主要为激活函数。设置阈值 1e-2。

  4. 累加函数。设置阈值 1e-2。

代码 2-3

def TestABF16(OpTest):
    #...
    def test_check_grad(self):
        self.check_grad(['X'], 'Out', max_relative_error=1e-2)

三、验证单测添加是否正确

3.1 本地环境验证

建议优先通过本地环境进行调试。

  1. 编译时,请打开测试选项 -DWITH_TESTING=ON ,并使用 make -j$(nproc)完成 Paddle 编译

    cmake .. -DWITH_GPU=ON -DWITH_TESTING=ON
    make -j $(nproc)
    
  2. pip 安装编译好的 whl 包,位于 build/python/dist 下。

    pip install python/dist/paddlepaddle-0.0.0-cpXX-cpXXm-linux_x86_64.whl
    
  3. 运行单测,验证是否通过。

    # 指定单测
    make test ARGS="-R test_mul_op -V"   # test_xxx 是单测文件名称
    或
    ctest -R test_mul_op [-V] #-V 可以打印详细的测试信息
    
  4. 如果通过,则添加正确;如果没有通过,请根据报错信息完成修改。

3.2 CI 验证

  1. 提交 PR 以后,CI 会对本次修改进行检查,出错的单测将被报出,可在全量日志中这次报错是否与本次修改有关

3.3 特定 CI 的 Approve

  1. 对于单测精度阈值(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