【深度强化学习】关于同一设备上cpu和gpu计算结果不一致问题


打假:基于pytorch的代码在GPU和CPU上训练时,训练输出结果不同问题
这里他说的话是没错的,但结论是错的,训练结果还是会不一致的。

问题描述

发现控制随机种子和所有超参数后,以及随机数都让cpu来产生,但实际运行后还是发现,同样的代码,结果还是不一致的问题。

代码:ppo代码(原代码为cuda版本)
动手学强化学习 github代码
只将device修改为如下,就为cpu版本

device = torch.device("cpu")    

此时上一版为cpu版本。

为了保持在环境采样中选择的一致性,在采样时还是选择了cpu来采样,只有训练模型时利用gpu来训练。此时为cpu+gpu版本。
结果每轮eposide的return如下:第一行为cpu+gpu版本。第二行cpu版本。
在这里插入图片描述
在第56列数据时出现了数据不一样的结果。

当然这里也验证了cuda版本的情况:
在第一个return数据就和cpu版本的不一致了。
(action第二个就不一致了,原因:第1,选择action时,使用forward了(即:actor(state)),利用cpu的forward 和利用cuda的forward,在结果上有细小差别(后面证明)。第2,后续更新还是利用了forward,加大了区别。)

关于seed: 跟原文一致

不考虑在不同设备上复现的话,只考虑在同一设备上复现的话:
在此代码:只需设置

torch.manual_seed(0) 
以及环境的随机种子env.reset(seed=0)

因为使用的是to.(device)代码,所以都是从cpu上随机产生随机数字,然后再移到其他设备,所以这里不需要设置cuda生成随机数相同。
举例:

import os
import torch
import torch.nn as nn
import torch.optim as optim
torch.manual_seed(0)

# 定义一个简单的模型
class SimpleModel(nn.Module):
    def __init__(self):
        super(SimpleModel, self).__init__()
        self.linear = nn.Linear(1, 1)

    def forward(self, x):
        return self.linear(x)

# 创建模型和优化器
model = SimpleModel().to("cuda") #加不加to("cuda")结果一样

# 输出模型参数
print("Model weights:", model.linear.weight.item())
### Model weights: -0.007486820220947266

补充:万能seed

使用在cuda上生成的随机数的情况:
需设置:

torch.cuda.manual_seed(seed) 

在这里插入图片描述
若是有多个gpu:
则需设置

torch.cuda.manual_seed_all(seed)

由于每个gpu的性能和硬件不同而导致的选择神经网络加速算法不同
cudnn:CUDA Deep Neural Network library(gpu加速库)
还需设置:

    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False 
    torch.backends.cudnn.enabled = False

意思是不适用cudnn加速库,使用确定性算法,且不选择速度最快的一次。
默认为:
在这里插入图片描述
也就是说,大多数情况下,第二个基准算法为了追求快,不追求同一设备每次结果一致的话可以设置为True,当然默认我们想要同一设备上结果一致,默认为False。

如果有import random
还需设置

random.seed(seed)

另外的配置:

哈希种子(没看出加了和没加有什么区别)
另外这个值在环境中默认是没有的。

os.environ['PYTHONHASHSEED'] = str(seed)

猜测:
这里是为Python脚本配置一个确定的哈希值。
就像:给一个值一个哈希数一样

# 随机一个hash值
print(hash('hello')) #-1149467266229358681

另外的两个配置,详细见:最靠谱的pytorch训练可复现性设置方案
万能seed:

def set_seed(seed):
    # seed init.
    random.seed(seed)
    np.random.seed(seed)
    os.environ['PYTHONHASHSEED'] = str(seed)

    # torch seed init.
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)

    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False
    torch.backends.cudnn.enabled = False # train speed is slower after enabling this opts.

    # https://pytorch.org/docs/stable/generated/torch.use_deterministic_algorithms.html
    os.environ['CUBLAS_WORKSPACE_CONFIG'] = ':16:8'

    # avoiding nondeterministic algorithms (see https://pytorch.org/docs/stable/notes/randomness.html)
    torch.use_deterministic_algorithms(True)

set_seed(11)

还要加上环境里的env 的seed。env.reset(seed =0 ) #gym0.26.2版本后

问题结论

参考:cuda官方文档

根据NVIDIA的文档,CPU和GPU计算结果不一致的原因主要与浮点数的表示和运算方式有关。总结如下:

1、浮点数表示差异:CPU和GPU使用不同的浮点数表示标准,如IEEE
754。这些标准在处理浮点数时可能会有细微的差异,尤其是在处理极端情况或边界值时。

2、舍入误差:在浮点数运算中,由于精度限制,舍入误差是不可避免的。CPU和GPU可能在舍入策略上有所不同,导致结果的微小差异。

3、运算精度:CPU和GPU可能在支持的运算精度上有所不同。例如,某些GPU可能更倾向于使用单精度浮点数,而CPU可能更倾向于使用双精度浮点数。这种差异可能导致计算结果的不同。

4、硬件架构差异:CPU和GPU的硬件架构差异可能导致它们在执行相同计算任务时的性能和准确性有所不同。GPU通常设计为并行处理大量数据,而CPU则更侧重于通用计算。

5、优化和近似算法:在某些情况下,GPU可能会使用特定的优化或近似算法来提高性能,这可能会影响计算结果的准确性。

6、软件和驱动程序差异:CPU和GPU的软件栈和驱动程序也可能导致计算结果的差异。不同的编译器优化、库函数实现等都可能影响最终的计算结果。

说人话,就是这种计算结果不一致基本避免不了。

我单步调试后确实如此,两者几乎采取的随机数都是一样的

在我的两个例子中:打印return的结果:
在这里插入图片描述
发现前面55个计算结果都一样,而后面的数开始不一样了。

cpu和gpu差异来源分析

浮点数精度的差异

先普及一下浮点数含义:

参考:
float的精度和取值范围

import numpy as np
print(0.1 * 6)
print(6 / 10 )
a = 0.1 * 6
print(np.array([a])[0]) #默认是float64
print(np.array([a], dtype=np.float32)[0])  # 32位浮点数可以表示小数点后7位
print('{}'.format(np.array([a], dtype=np.float32)[0]))  # 32位浮点数可以表示小数点后7位
print(np.array([a], dtype=np.float64)[0])   # 64位浮点数可以表示小数点后16位
print(np.array([a], dtype=np.float32)[0]+0.0000000000000001)   
print(np.array([a], dtype=np.float64)[0]+0.0000001)
print('{:.8f}'.format(1e-8))
print(0.6 - np.array([a], dtype=np.float32)[0])
'''
0.6000000000000001
0.6
0.6000000000000001
0.6
0.6000000238418579
0.6000000000000001
0.600000023841858
0.6000001
0.00000001
-2.384185793236071e-08
'''
print(0.1 *6 )   #小数后有16位,精确到15位
print(0.1 * 0.1)   #小数后有18位,精确到17位
print(0.1 * 3)  #小数后有17位,精确到16位
## 所以 float32 可以精确到小数点后7位,float64可以精确到小数点后15-17 位
'''
0.6000000000000001
0.010000000000000002
0.30000000000000004

'''

结论:float32 可以精确到小数点后7位,
float64可以精确到小数点后15-17 位。

上述cuda官方上有说,cpu可能是可以精确到float80位,然后截断到64位,所以可能会运算的比cuda更加精确。
但实际上:
在这里插入图片描述
两者的在同一浮点精度下,得到的数是相等的。

为了避免这个偏差,我这里设置为相同的精度运算方法float32,且每一个数都由cpu产生。(默认cpu产生,即去掉了device=的操作)

补充报错:Expected all tensors to be on the same device!

注意:张量的操作都只能同时在同一设备上进行,否则报错。

这个操作往往很难发现:
比如:下面这种情况却不会报错。因为PyTorch 在执行这种操作时会自动处理设备不匹配。它会将所有的操作数移动到同一设备上,然后执行操作。
在这里插入图片描述

而我如果进行大量的操作时:就会出来,所以还是要分开来运算。

在这里插入图片描述
为了避免出现因为pytorch自动的处理,和为了累计误差,我下面都会尽量使用大量的操作。

常见运算上的差异

这里看下常见的加法,幂运算exp,矩阵乘法matmul运算上的差异

import torch
import gym
torch.manual_seed(0)
env_name = 'CartPole-v1' #动作是
env = gym.make(env_name)
s = env.reset(seed=0)
# print(s)
print('原始值:',s[0]) #tensor
print('原始值的为float32,小数后7位为精确值,第8位开始乱数字,例:第一个值为{}'.format(s[0][0])) #numpy.float32
s_cpu = torch.tensor(s[0], dtype=torch.float32).to('cpu')
s_cuda = torch.tensor(s[0], dtype=torch.float32).to('cuda')


for i in range(10000): #1000时没有
    nn =torch.randn(4, dtype=torch.float32)
    s_cpu +=  torch.exp(nn.to('cpu'))
    s_cuda += torch.exp(nn.to('cuda'))
s_cuda_cpu = s_cuda.to('cpu')
print(s_cpu)
print(s_cuda)
print(s_cuda_cpu)
print("torch.exp累加误差:",torch.max(torch.abs(s_cuda_cpu - s_cpu)).item())

print('原始值:',s[0]) #tensor
s_cpu = torch.tensor(s[0], dtype=torch.float32).to('cpu')
s_cuda = torch.tensor(s[0], dtype=torch.float32).to('cuda')
for i in range(10000): #
    nn =torch.randn(4, dtype=torch.float32)
    s_cpu +=  nn.to('cpu')
    s_cuda += nn.to('cuda')
s_cuda_cpu = s_cuda.to('cpu') #将CUDA结果移回CPU

print(s_cpu)
print(s_cuda)
print(s_cuda_cpu)
print("随机数累加误差:",torch.max(torch.abs(s_cuda_cpu - s_cpu)).item())
#print(torch.max(torch.abs(s_cuda_cpu - s_cuda).item())) #报错: cuda和cpu的tensor不能同时运算

print('原始值:',s[0]) #tensor
s_cpu = torch.tensor(s[0], dtype=torch.float32).to('cpu')
s_cuda = torch.tensor(s[0], dtype=torch.float32).to('cuda')
for i in range(2): #1000时没有
    nn =torch.randn(4, dtype=torch.float32)
    s_cpu +=  torch.matmul(nn.to('cpu'),nn.to('cpu').t())
    s_cuda += torch.matmul(nn.to('cuda'),nn.to('cuda').t())
s_cuda_cpu = s_cuda.to('cpu')
print(s_cpu)
print(s_cuda)
print(s_cuda_cpu)
print("torch.matmul累加误差:",torch.max(torch.abs(s_cuda_cpu - s_cpu)).item())

结果为:
在这里插入图片描述
在累加的操作下,差异几乎为0
幂运算的操作下,会出现差异
矩阵运算的操作下,会出现差异

我在实验的时候发现,将第一个和第二个顺序互换,有时会发现,exp的差异为0。

累加运算的差异

为了继续验证exp的运算有差异:
使随机的数增多:
在这里插入图片描述
结果:有差异了。

exp运算的差异

为了继续验证exp的运算有差异:
使随机的数增多:

import torch

torch.manual_seed(0)
# 创建一个随机张量
x = torch.randn(10000, dtype=torch.float32)
# 在CPU上进行累加
x_cpu = x.clone() #.clone() 复制张量
sum_cpu = torch.zeros_like(x_cpu)
#print(sum_cpu)
for _ in range(10000):
    sum_cpu += torch.exp(x_cpu)   #torch.mean()
# 在CUDA上进行累加
if torch.cuda.is_available():
    x_cuda = x.to('cuda')
    sum_cuda = torch.zeros_like(x_cuda).to('cuda') #zeros_like()创建一个与输入张量相同大小的全0张量
    #print(sum_cuda)
    for _ in range(10000):
        sum_cuda += torch.exp(x_cuda)
    # 将CUDA结果移回CPU
    sum_cuda_cpu = sum_cuda.to('cpu')
    # 计算差异
    diff = torch.max(torch.abs(sum_cpu - sum_cuda_cpu))
    print(f"Max difference between CPU and CUDA: {diff.item()}")
else:
    print("CUDA is not available.")

结果:差异变的很明显了。
在这里插入图片描述

matmul运算的差异

同理:

import torch
torch.manual_seed(0)
# 创建一个随机张量
x = torch.randn(1000, 1000, dtype=torch.float32)
# 在CPU上计算
x_cpu = x.to('cpu')
result_cpu = torch.matmul(x_cpu, x_cpu.t()) #矩阵乘法
# 在CUDA上计算
if torch.cuda.is_available():
    x_cuda = x.to('cuda')
    result_cuda = torch.matmul(x_cuda, x_cuda.t())
    # 将CUDA结果移回CPU
    result_cuda_cpu = result_cuda.to('cpu')
    # 计算差异
    diff = torch.max(torch.abs(result_cpu - result_cuda_cpu))
    print(f"Max difference between CPU and CUDA: {diff.item()}")
else:
    print("CUDA is not available.")

差异为:
在这里插入图片描述
差异也变大了。

`结论:这里仅简单得到一些常见的运算差异,据此推测,其他的运算在大量的运算下,差异会越来越明显。

forward上的差异(激活函数的运算差异)

这里用一个最简单的单层qnet表示其差异,激活函数为relu。

.cuda() 和to(‘cuda’)的结果一样,只是to(‘cuda:0’),可以指定哪个gpu,我这里单个gpu,所以两个函数一个意思。

代码: 因为这里没有用到反向传播forward,所以在第一次forward后,模型的实例没有变。

import torch
import torch.nn.functional as F

# 设置随机种子
torch.manual_seed(0)

# 定义模型
class ValueNet(torch.nn.Module):
    def __init__(self, state_dim, hidden_dim):
        super(ValueNet, self).__init__()
        self.fc1 = torch.nn.Linear(state_dim, hidden_dim)
        self.fc2 = torch.nn.Linear(hidden_dim, 1)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        return self.fc2(x)

# 设置模型和数据
state_dim = 10
hidden_dim = 20
model = ValueNet(state_dim, hidden_dim)
input_data = torch.randn(100, state_dim,dtype=torch.float32)  # 假设有100个样本

# 在CPU上运行
model.cpu()
input_data = input_data.cpu()
cpu_output = model(input_data)

# 在GPU上运行
if torch.cuda.is_available():
    model.cuda()
    input_data = input_data.cuda()
    gpu_output = model(input_data)
else:
    print("No GPU available")

# 比较输出结果
if torch.cuda.is_available():
    # 将GPU输出转移到CPU上进行比较
    gpu_output_cpu = gpu_output.cpu()
    # 计算差异
    difference = torch.abs(cpu_output - gpu_output_cpu)
    print("Max difference:", difference.max().item())
    print("Average difference:", difference.mean().item())
else:
    print("No GPU available for comparison")

单次运行结果差异:
在这里插入图片描述

补充: .to(device)在普通张量和类上的差异:

张量的to(device):它会创建一个新的张量副本,并将这个副本移动到指定的设备上。原始张量不会被修改。

在类(模型)的to(device):修改模型实例的存储位置(参数和缓冲区),模型实例本身并不会被重新创建。

所以上文的代码可以写成这样:

## gpu 与 cpu 差异来源分析
import torch
import torch.nn.functional as F
import copy

# 设置随机种子
torch.manual_seed(0)

# 定义模型
class ValueNet(torch.nn.Module):
    def __init__(self, state_dim, hidden_dim):
        super(ValueNet, self).__init__()
        self.fc1 = torch.nn.Linear(state_dim, hidden_dim)
        self.fc2 = torch.nn.Linear(hidden_dim, 1)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        return self.fc2(x)

# 设置模型和数据
state_dim = 10
hidden_dim = 20
model = ValueNet(state_dim, hidden_dim)
input_data = torch.randn(100, state_dim,dtype=torch.float32)  # 假设有100个样本
model_cpu = copy.deepcopy(model) #复制模型
model_cuda = model.to('cuda')
print("Model_cpu weights:", model_cpu.fc1.weight[0][0])
print("Model_gpu weights:", model_cuda.fc1.weight[0][0])
# 在CPU上运行

input_data_cpu = input_data.to('cpu')
input_data_cuda = input_data.to('cuda')

cpu_output = model_cpu(input_data_cpu)

gpu_output = model_cuda(input_data_cuda)

gpu_output_cpu = gpu_output.to('cpu')
# 计算差异
difference = torch.abs(cpu_output - gpu_output_cpu)
print("Max difference:", difference.max().item())
print("Average difference:", difference.mean().item())

在这里插入图片描述
确实保证了模型的参数一致,有了差异。

backward上的差异

为了剔除掉forward()函数上的激活函数影响,这里使用最简单的模型

# 定义一个简单的线性模型
model = nn.Linear(1, 1) # 输入维度是1,输出维度是1

来验证其差异性:

# 探讨backward()函数的差异
import torch
import torch.nn as nn
import torch.optim as optim
import copy
#设置随机种子
torch.manual_seed(0)

# 生成一些随机的输入和标签
x = torch.randn(100, 1,dtype=torch.float32) # 100个样本,每个样本有1个特征  randn
y = 3 * x + 5 + torch.randn(100, 1,dtype=torch.float32) # 100个样本,每个样本有1个标签,服从 y = 3x + 5 + 噪声 的分布

# 定义一个简单的模型
class SimpleModel(nn.Module):
    def __init__(self):
        super(SimpleModel, self).__init__()
        self.linear = nn.Linear(1, 1)

    def forward(self, x):
        return self.linear(x)
# 定义一个简单的线性模型
model = nn.Linear(1, 1) # 输入维度是1,输出维度是1
#model = SimpleModel()  # 也可以使用此模型,结果一样,但最好不要同时设置,即 model = nn.Linear(1, 1) ; model = SimpleModel()
#因为每次调用 nn.Linear 都会使用随机数生成器来初始化权重,会使结果不一致
model_cpu = copy.deepcopy(model)
model_cuda = model.to('cuda')   
# 定义一个均方误差损失函数
criterion = nn.MSELoss()
# 定义一个随机梯度下降优化器
optimizer_cpu = optim.SGD(model_cpu.parameters(), lr=0.01) # 学习率是0.01
optimizer_cuda = optim.SGD(model_cuda.parameters(), lr=0.01) # 学习率是0.01

epochs = 1000
# 在CPU上训练
for epoch in range(epochs):
    output = model_cpu(x)
    loss = criterion(output, y)
    optimizer_cpu.zero_grad()
    loss.backward()
    optimizer_cpu.step()
# 在GPU上训练
for epoch in range(epochs):
    output = model_cuda(x.to('cuda'))
    loss = criterion(output, y.to('cuda'))
    optimizer_cuda.zero_grad()
    loss.backward()
    optimizer_cuda.step()
model_cuda_cpu = model_cuda.to('cpu')

x_ = torch.randn(100, 1,dtype=torch.float32)
#print(x_)
cpu_output = model_cpu(x_)
gpu_output = model_cuda_cpu(x_)

diff =torch.max(torch.abs(cpu_output - gpu_output)).item()
print("Max difference:{}".format(diff))
# 打印模型参数
print( 'cpu上:',model_cpu.weight.item(), model_cpu.bias.item()) #用SimpleModel()时,要改为model_cpu.linear.weight.item(), model_cpu.linear.bias.item()
print('cuda上:',model_cuda.weight.item(), model_cuda.bias.item())

结果为:
在这里插入图片描述
在epoch为1000时,差异还是0,在10000时就有了细小的差异。

总结或建议

1、在一般使用中,可以不必追求cpu和gpu计算的结果一致性,也也避免不了,且cpu和gpu导致的细小差别,在训练的效果上几乎没有区别。

2、同时,在同一台设备上,我们尽量要求该程序的结果能复现,是为了更好修改超参数。(见:本文万能seed,适用于单机多卡)

3、不必追求在不同的设备上能复现一致结果,最终的效果在相同的超参数和输入下,输出的结果也相差无几。

4、cpu和gpu在设计时的目的也不同,有差异理所应当,这里本文只分析了在计算上和训练神经网络时的出现的差异,仅作参考。

补充:写在后面

–24.5.24
发现了一个区别:
还是此代码:
代码:ppo代码(原代码为cuda版本)
动手学强化学习 github代码

现象:
在环境’CartPole-v0’的情况下:
我将while not done : 改成了 如下情况
在这里插入图片描述
发现在cpu版本的情况下,其他参数不变,依然能够收敛;
在这里插入图片描述

而cuda版本下,就不收敛了。
在这里插入图片描述
而cpu+cuda 的版本也不收敛。

这里的情况就是在done这个状态相当于没有的情况下,cpu依然能够学习到可以收敛的参数。
结论:
1、猜测cpu的计算精度确实比cuda要高的多。
2、状态的设置对于模型的收敛至关重要。

–24.5.30–
这里状态done的信息应该还是有的,只是本来done为true是这个轨迹就结束了,这里是继续增加轨迹的长度,即可能的结果done: False …False…True (本来这里结束)False False…True…False。直到结束。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值