Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【Hackathon 5th No.1】 为 Paddle 新增 copysign API (RFC update) #793

Merged
merged 3 commits into from
Jan 8, 2024

Conversation

Copy link

paddle-bot bot commented Dec 28, 2023

你的PR提交成功,感谢你对开源项目的贡献!
请检查PR提交格式和内容是否完备,具体请参考示例模版
Your PR has been submitted. Thanks for your contribution!
Please check its format and content. For this, you can refer to Template and Demo.

| torch.bfloat16 | torch.bfloat16 | torch.bfloat16 | torch.bfloat16 |

+ 可以发现,在整型输入时,numpy和pytorch的行为略有不同:pytorch面对整型输入,均保持`float32`作为输出,而numpy在整型输入时,仅当dtype为`int16`时,输出的dtype与pytorch对齐(均为`float32`)。
+ 另外,pytorch支持整型(包括bool)的反向传播(但是paddle目前似乎并未对此作限制)。对于浮点数,输入和输出类型保持一致。
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里pytorch应该是不支持反向?

Copy link
Contributor Author

@cocoshe cocoshe Dec 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里pytorch应该是不支持反向?

pytorch不支持整型反向(或者说对所有算子都不支持整型和requires_grad=True一起满足)

>>> x = torch.tensor([10], dtype=torch.int32, requires_grad=True)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: Only Tensors of floating point and complex dtype can require gradients

pytorch支持bfloat16反向:

>>> import torch
>>> x = torch.tensor([10], dtype=torch.bfloat16, requires_grad=True)
>>> y = torch.tensor([-10], dtype=torch.bfloat16, requires_grad=True)
>>> z = torch.copysign(x,y).sum()
>>> z.backward()
>>> x.grad
tensor([-1.], dtype=torch.bfloat16)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pr中对应的文字需要改动下哈,和这里的描述有出入

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pr中对应的文字需要改动下哈,和这里的描述有出入

sry,少打了一个"不"字


而且numpy和pytorch遇到整型输入,得到输出dtype常常不同,需要确定paddle的实现。

+ 反向传播的dtype是否一直保持不变?如果是,那么遇到dtype为整型的时(paddle没有严格限制反向传播过程的dtype不能为整型,pytorch有强制限制反向传播过程不能为整型),求梯度会有f(x,y)得到float类型,就变了。paddle目前支持反向传播过程中数据类型发生变化吗?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

梯度输出的类型,理论上还是应该最终由grad OP控制吧,如果从OP的设计上是统一类型的输入输出,那么结果应该是不变的?

paddle目前支持反向传播过程中数据类型发生变化吗?

这个问题,仍然需要拆分到grad OP的视角来看,由grad OP控制,比如下面这个例子,可以试着把x,y分别切换成float / int类型看看结果

import paddle

x = paddle.ones((2,3), dtype='int32')
y = paddle.full((2,3), 5, dtype='int32')
x.stop_gradient=False
y.stop_gradient=False

z1 = x * y
print(z1)
z2 = z1 / 10
print(z2)
loss = z2.sum()
loss.backward()

print(x.grad)
print(y.grad)

Copy link
Contributor Author

@cocoshe cocoshe Jan 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

梯度输出的类型,理论上还是应该最终由grad OP控制吧,如果从OP的设计上是统一类型的输入输出,那么结果应该是不变的?

paddle目前支持反向传播过程中数据类型发生变化吗?

这个问题,仍然需要拆分到grad OP的视角来看,由grad OP控制,比如下面这个例子,可以试着把x,y分别切换成float / int类型看看结果

import paddle

x = paddle.ones((2,3), dtype='int32')
y = paddle.full((2,3), 5, dtype='int32')
x.stop_gradient=False
y.stop_gradient=False

z1 = x * y
print(z1)
z2 = z1 / 10
print(z2)
loss = z2.sum()
loss.backward()

print(x.grad)
print(y.grad)

嗯嗯我尝试了一下,前两天我去仔细看了下phi算子的注册逻辑,但是就是在这里,有个地方不太理解是怎么实现的。就像之前提到的,在写kernel的时候,我们其实一开始就要给out申请空间,这里就要确定out的类型了:
dev_ctx.template Alloc<T>(out);

比如在你这个z2 = z1 / 10的时候,z1int32,但是z2却是一个float32
我看到文档里面提到除法就是divide包了一层魔法函数,然后去看了下devide的kernel

它注册的时候:

PD_REGISTER_KERNEL(divide,
                   CPU,
                   ALL_LAYOUT,
                   phi::DivideKernel,
                   float,
                   double,
                   int8_t,
                   uint8_t,
                   int16_t,
                   int,
                   int64_t,
                   bool,
                   complex64,
                   complex128) {}

也并没有去指定他的输出类型,采用的是默认的,也就是说template<T, Context>中的T分别注册了float,double,int8_t,uint8_t,int16_t,int,int64_t,bool,complex64,complex128这些类型,每次注册的时候,由于都是默认的,所以kernel的参数:const DenseTensor& x,const DenseTensor& y,DenseTensor* out都应该是当前注册时候的类型,例如注册int32的时候,输入和输出的tensor dtype都必须全为int32

我试了一下直接调用divide这个api:

>>> x
Tensor(shape=[2, 3], dtype=int32, place=Place(cpu), stop_gradient=False,
       [[1, 1, 1],
        [1, 1, 1]])
>>> y
Tensor(shape=[2, 3], dtype=int32, place=Place(cpu), stop_gradient=False,
       [[5, 5, 5],
        [5, 5, 5]])
>>> z1
Tensor(shape=[2, 3], dtype=int32, place=Place(cpu), stop_gradient=False,
       [[5, 5, 5],
        [5, 5, 5]])
>>> ################## z1 是 int32,走的是魔法函数 #####################
>>> z1 / 10
Tensor(shape=[2, 3], dtype=float32, place=Place(cpu), stop_gradient=False,
       [[0.50000000, 0.50000000, 0.50000000],
        [0.50000000, 0.50000000, 0.50000000]])

>>> ################## z1 是 int32,“10”是数字,需要tensor,错误 #####################
>>> paddle.divide(z1, 10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/coco/miniconda3/envs/exp/lib/python3.11/site-packages/paddle/tensor/math.py", line 895, in divide
    return _C_ops.divide(x, y)
           ^^^^^^^^^^^^^^^^^^^
ValueError: (InvalidArgument) divide(): argument 'y' (position 1) must be Tensor, but got int (at /paddle/paddle/fluid/pybind/eager_utils.cc:1334)

>>> ################## z1 是 int32,“10”是int64,类型不匹配 #####################
>>> paddle.divide(z1, paddle.to_tensor(10))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/coco/miniconda3/envs/exp/lib/python3.11/site-packages/paddle/tensor/math.py", line 895, in divide
    return _C_ops.divide(x, y)
           ^^^^^^^^^^^^^^^^^^^
ValueError: (InvalidArgument) The type of data we are trying to retrieve (int64) does not match the type of data (int32) currently contained in the container.
  [Hint: Expected dtype() == phi::CppTypeToDataType<T>::Type(), but received dtype():7 != phi::CppTypeToDataType<T>::Type():9.] (at /paddle/paddle/phi/core/dense_tensor.cc:171)

>>> ################## z1 是 int32,“10”是int32,成功,类型匹配 #####################
>>> paddle.divide(z1, paddle.to_tensor(10,dtype=paddle.int32))
Tensor(shape=[2, 3], dtype=int32, place=Place(cpu), stop_gradient=False,
       [[0, 0, 0],
        [0, 0, 0]])

>>> ################## z1 是 int32,“10”是float32,错误,类型不匹配 #####################
>>> paddle.divide(z1, paddle.to_tensor(10,dtype=paddle.float32))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/coco/miniconda3/envs/exp/lib/python3.11/site-packages/paddle/tensor/math.py", line 895, in divide
    return _C_ops.divide(x, y)
           ^^^^^^^^^^^^^^^^^^^
ValueError: (InvalidArgument) The type of data we are trying to retrieve (float32) does not match the type of data (int32) currently contained in the container.
  [Hint: Expected dtype() == phi::CppTypeToDataType<T>::Type(), but received dtype():7 != phi::CppTypeToDataType<T>::Type():10.] (at /paddle/paddle/phi/core/dense_tensor.cc:171)

为什么直接z1 / 10能够正常呢?难道是在进kernel之前把输入的两个dtype检测同步了一下吗?因为单从这个kernel来看,应该是仅支持所有输入、输出dtype相同

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

运算符和api在Paddle中有些差异,感兴趣具体可以看tensor__div__method的实现,中间有插入额外cast操作。不过我理解上述case主要目的和这个除法无关,主要想表示int tensor在网络中参与反向时的情况。

Copy link
Contributor Author

@cocoshe cocoshe Jan 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

运算符和api在Paddle中有些差异,感兴趣具体可以看tensor__div__method的实现,中间有插入额外cast操作。

嗯嗯谢谢~,fluid部分我还不是很熟悉。

不过我理解上述case主要目的和这个除法无关,主要想表示int tensor在网络中参与反向时的情况。

嗯嗯明白,backward确实取决于grad op的操作,但是目前大部分grad op也都是输入和输出dtype必须保持一致吧。
例如上面您说的那个例子:

  1. 当x和y是int32的时候:
>>> x
Tensor(shape=[2, 3], dtype=int32, place=Place(cpu), stop_gradient=False,
       [[1, 1, 1],
        [1, 1, 1]])
>>> y
Tensor(shape=[2, 3], dtype=int32, place=Place(cpu), stop_gradient=False,
       [[5, 5, 5],
        [5, 5, 5]])
>>> z1
Tensor(shape=[2, 3], dtype=int32, place=Place(cpu), stop_gradient=False,
       [[5, 5, 5],
        [5, 5, 5]])
>>> z2
Tensor(shape=[2, 3], dtype=float32, place=Place(cpu), stop_gradient=False,
       [[0.50000000, 0.50000000, 0.50000000],
        [0.50000000, 0.50000000, 0.50000000]])
>>> loss
Tensor(shape=[], dtype=float32, place=Place(cpu), stop_gradient=False,
       3.)
>>> loss.backward()
>>> x.grad
Tensor(shape=[2, 3], dtype=int32, place=Place(cpu), stop_gradient=False,
       [[0, 0, 0],
        [0, 0, 0]])
>>> y.grad
Tensor(shape=[2, 3], dtype=int32, place=Place(cpu), stop_gradient=False,
       [[0, 0, 0],
        [0, 0, 0]])
>>>

结果x.grady.grad输出是全0,是错误的,类型是int32

  1. 当x和y是float32的时候:
>>> x
Tensor(shape=[2, 3], dtype=float32, place=Place(cpu), stop_gradient=False,
       [[1., 1., 1.],
        [1., 1., 1.]])
>>> y
Tensor(shape=[2, 3], dtype=float32, place=Place(cpu), stop_gradient=False,
       [[5., 5., 5.],
        [5., 5., 5.]])
>>> z1
Tensor(shape=[2, 3], dtype=float32, place=Place(cpu), stop_gradient=False,
       [[5., 5., 5.],
        [5., 5., 5.]])
>>> z2
Tensor(shape=[2, 3], dtype=float32, place=Place(cpu), stop_gradient=False,
       [[0.50000000, 0.50000000, 0.50000000],
        [0.50000000, 0.50000000, 0.50000000]])
>>> loss
Tensor(shape=[], dtype=float32, place=Place(cpu), stop_gradient=False,
       3.)
>>> x.grad
Tensor(shape=[2, 3], dtype=float32, place=Place(cpu), stop_gradient=False,
       [[0.50000000, 0.50000000, 0.50000000],
        [0.50000000, 0.50000000, 0.50000000]])
>>> y.grad
Tensor(shape=[2, 3], dtype=float32, place=Place(cpu), stop_gradient=False,
       [[0.10000000, 0.10000000, 0.10000000],
        [0.10000000, 0.10000000, 0.10000000]])
>>>

可以发现两个梯度是正确的,类型是float32

  1. 当x和y均为float64的时候:
>>> x
Tensor(shape=[2, 3], dtype=float64, place=Place(cpu), stop_gradient=False,
       [[1., 1., 1.],
        [1., 1., 1.]])
>>> y
Tensor(shape=[2, 3], dtype=float64, place=Place(cpu), stop_gradient=False,
       [[5., 5., 5.],
        [5., 5., 5.]])
>>> z1
Tensor(shape=[2, 3], dtype=float64, place=Place(cpu), stop_gradient=False,
       [[5., 5., 5.],
        [5., 5., 5.]])
>>> z2
Tensor(shape=[2, 3], dtype=float64, place=Place(cpu), stop_gradient=False,
       [[0.50000000, 0.50000000, 0.50000000],
        [0.50000000, 0.50000000, 0.50000000]])
>>> loss
Tensor(shape=[], dtype=float64, place=Place(cpu), stop_gradient=False,
       3.)
>>> x.grad
Tensor(shape=[2, 3], dtype=float64, place=Place(cpu), stop_gradient=False,
       [[0.50000000, 0.50000000, 0.50000000],
        [0.50000000, 0.50000000, 0.50000000]])
>>> y.grad
Tensor(shape=[2, 3], dtype=float64, place=Place(cpu), stop_gradient=False,
       [[0.10000000, 0.10000000, 0.10000000],
        [0.10000000, 0.10000000, 0.10000000]])
>>>

这样看来,divide的grad kernel只能接受输入和输出dtype相同的情况。所以在backward计算的时候,似乎基本都不太支持dtype发生改变。

感觉应该是grad kernel中存在forward时候的下一个op(backward时的上一个op)的dout,作为其中的一个参数,然后计算当前输入变量的梯度,如果中间类型发生变化,那么对grad kernel来说,又是变成了"输入两个变量类型不同"的情况。


+ 反向传播的dtype是否一直保持不变?如果是,那么遇到dtype为整型的时(paddle没有严格限制反向传播过程的dtype不能为整型,pytorch有强制限制反向传播过程不能为整型),求梯度会有f(x,y)得到float类型,就变了。paddle目前支持反向传播过程中数据类型发生变化吗?

单从功能上来看,`copysign`实现的逻辑比较简单:取第一个变量的绝对值的大小,取第二个变量符号,两者拼接。感觉从功能上来看,可以不拘泥于跟竞品一样调用标准库的`std::copysign`,而是直接在Functor中判断来实现,而且输入和输出dtype保持相同。
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

是否可以仍然调用标准库,只是kernel计算逻辑里额外做下cast;包括这里设计的方案都可以尝试下,看看目前实现起来是否有堵点,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

是否可以仍然调用标准库,只是kernel计算逻辑里额外做下cast;包括这里设计的方案都可以尝试下,看看目前实现起来是否有堵点,

现在就是在kernel里面加了这个:

  using U = typename std::conditional_t<std::is_integral<T>::value, float, T>;
  dev_ctx.template Alloc<U>(out);

就是根据注册的dtype来申请输出的内存空间,如果注册的dtype为整型相关的,那么就申请float类型的空间给输出,这里行为跟pytorch的行为一样:

x y np.copysign(x,y)/torch.copysign(x,y) grad_x(grad_y)
np.uint8 np.uint8 np.float16 /
torch.uint8 torch.uint8 torch.float32 /
np.int8 np.int8 np.float16 /
torch.int8 torch.int8 torch.float32 /
np.int16 np.int16 np.float32 /
torch.int16 torch.int16 torch.float32 /
np.int32 np.int32 np.float64 /
torch.int32 torch.int32 torch.float32 /
np.int64 np.int64 np.float64 /
torch.int64 torch.int64 torch.float32 /
np.float16 np.float16 np.float16 /
torch.float16 torch.float16 torch.float16 torch.float16
np.float32 np.float32 np.float32 /
torch.float32 torch.float32 torch.float32 torch.float32
np.float64 np.float64 np.float64 /
torch.float64 torch.float64 torch.float64 torch.float64
np.complex64 np.complex64 / /
torch.complex64 torch.complex64 / /
np.complex128 np.complex128 / /
torch.complex128 torch.complex128 / /
np.bool np.bool np.float16 /
torch.bool torch.bool torch.float32 /
torch.bfloat16 torch.bfloat16 torch.bfloat16 torch.bfloat16

如果是浮点数或者float16,bfloat16这两个特殊类型,就保持输入什么dtype,输出就什么dtype,这里也跟pytorch实现的行为一致:

>>> ****************** float16 *******************
>>> x = paddle.to_tensor([10], dtype=paddle.float16)
>>> y = paddle.to_tensor([-10], dtype=paddle.float16)
>>> func(x,y)
Tensor(shape=[1], dtype=float16, place=Place(gpu:0), stop_gradient=True,
       [-10.])

>>> ****************** bfloat16 *******************
>>> x = paddle.to_tensor([10], dtype=paddle.bfloat16)
>>> y = paddle.to_tensor([-10], dtype=paddle.bfloat16)
>>> func(x,y)
Tensor(shape=[1], dtype=bfloat16, place=Place(gpu:0), stop_gradient=True,
       [-10.])

输入类型和输出类型一样。

然后下面是一些整型:

>>> x = paddle.to_tensor([10], dtype=paddle.uint8)
>>> y = paddle.to_tensor([10], dtype=paddle.uint8)
>>> func(x,y)
Tensor(shape=[1], dtype=float32, place=Place(gpu:0), stop_gradient=True,
       [10.])
>>> x = paddle.to_tensor([10], dtype=paddle.int8)
>>> y = paddle.to_tensor([10], dtype=paddle.int8)
>>> func(x,y)
Tensor(shape=[1], dtype=float32, place=Place(gpu:0), stop_gradient=True,
       [10.])
>>> x = paddle.to_tensor([10], dtype=paddle.int16)
>>> y = paddle.to_tensor([10], dtype=paddle.int16)
>>> func(x,y)
Tensor(shape=[1], dtype=float32, place=Place(gpu:0), stop_gradient=True,
       [10.])
>>> x = paddle.to_tensor([10], dtype=paddle.int32)
>>> y = paddle.to_tensor([10], dtype=paddle.int32)
>>> func(x,y)
Tensor(shape=[1], dtype=float32, place=Place(gpu:0), stop_gradient=True,
       [10.])
>>> x = paddle.to_tensor([10], dtype=paddle.int64)
>>> y = paddle.to_tensor([10], dtype=paddle.int64)
>>> func(x,y)
Tensor(shape=[1], dtype=float32, place=Place(gpu:0), stop_gradient=True,
       [10.])
>>> 

可以看到输出都是float32。

现在就是目前的实现,仍然调用标准库,而且和pytorch对齐了的。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

从前面的讨论来看,大致存在两个做法:

  1. 代码PR中的实现,即int大类输出float32
  2. 这个RFC PR中写到的

copysign实现的逻辑比较简单:取第一个变量的绝对值的大小,取第二个变量符号,两者拼接。感觉从功能上来看,可以不拘泥于跟竞品一样调用标准库的std::copysign,而是直接在Functor中判断来实现,而且输入和输出dtype保持相同。

即有符号int仍输出有符号int,无符号int需要额外考虑,不过由于本身也不存在符号,似乎也可以保持原输入dtype

看到前面已经阐述了第一个方案具备可行性。想了解下第二种方案是否具备可行性呢。

目前主要还是想评估一下两个方案的优劣情况,确定这个API的最终行为

Copy link
Contributor Author

@cocoshe cocoshe Jan 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

从前面的讨论来看,大致存在两个做法:

  1. 代码PR中的实现,即int大类输出float32
  2. 这个RFC PR中写到的

copysign实现的逻辑比较简单:取第一个变量的绝对值的大小,取第二个变量符号,两者拼接。感觉从功能上来看,可以不拘泥于跟竞品一样调用标准库的std::copysign,而是直接在Functor中判断来实现,而且输入和输出dtype保持相同。

即有符号int仍输出有符号int,无符号int需要额外考虑,不过由于本身也不存在符号,似乎也可以保持原输入dtype

看到前面已经阐述了第一个方案具备可行性。想了解下第二种方案是否具备可行性呢。

目前主要还是想评估一下两个方案的优劣情况,确定这个API的最终行为

对,这两个方案应该都可以,只是想要对齐pytorch的行为的话,就是目前的版本;如果想要第二种做法的话,在调用static_cast<T>((std::copysign(x,y))应该就可以实现。保守一些的话应该第一种就可以,第二种的话相对来说更加符合直觉(输入整型,输出整型)。

无符号数在调用标准库的实现的时候,它应该已经自动先转为浮点数(而且是正数)了,而不会像float16或者bfloat16那样位运算的实现,所以应该不会影响结果。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cocoshe 你好,我们内部讨论了下,决定还是按语义符合直觉的方向去实现更好,,即第二种输入输出dtype一致的方案。辛苦修改下RFC和代码呢~

Copy link
Contributor

@zoooo0820 zoooo0820 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@zoooo0820 zoooo0820 merged commit 7d7e924 into PaddlePaddle:master Jan 8, 2024
1 check passed
@cocoshe cocoshe deleted the copysign_rfc_update branch January 8, 2024 06:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants