文章大部分内容来自PyTorch-CUDA Semantics,如若侵权请联系本人进行删除,本文仅用作个人学习之用,不做他用,转载请注明出处。
pytorch提供torch.cuda
来创建和运行CUDA操作。torch.cuda
会一直跟踪当前选中的GPU,所有分配的CUDA tensors都会默认在该GPU上创建分配内存空间。但是,仍然可以通过torch.cuda.device
来改变选中的GPU(即去选择另外一个GPU)。
一旦一个tensor被分配了空间,则可以操作这个tensor,而不需要管选中了哪个GPU(根据我的理解是:这个Tensor创建的时候已经指定了在哪个GPU上创建,所以之后并不需要再关心)。在这个tensor上的相关计算后的结果仍会保存在该tensor所在的GPU的存储空间中。
默认情况下,跨GPU的运算操作是不被允许的,除了copy_()
、to()
、cuda()
等涉及存储拷贝的函数。除非启用了点对点存储访问(peer-to-peer memory access),否则任何尝试在不同的GPU上执行相关计算的操作都会引发错误。
代码示例:
cuda = torch.device('cuda') # 默认的CUDA设备
cuda0 = torch.device('cuda:0')
cuda2 = torch.device('cuda:2') # GPU2
x = torch.tensor([1.,2.],device=cuda0) # 在cuda0上创建一个tensor x
y = torch.tensor([1.,2.]).cuda()
with torch.cuda.device(1):
a = torch.tensor([1.,2.],device=cuda) # 在GPU1上分配一个tensor
b = torch.tensor([1.,2.]).cuda() # 将tensor从CPU传输到GPU1
b2 = torch.tensor([1.,2.]).to(device=cuda) # 将tensor从CPU传输到GPU1
c = a + b
z = x + y
# 在GPU2上做tensor相关的操作
d = torch.randn(2,device=cuda2)
e = torch.randn(2).to(cuda2)
f = torch.randn(2).cuda(cuda2)
默认情况下,GPU的操作是异步的。在使用GPU的情况下调用某个函数(执行某个功能),操作会排队进入某个特定的GPU设备,但是不会等到以后执行(可能不是按进入队列的顺序来顺序执行)。因为GPU的并行计算的特性,这些操作可以在GPU中并行执行。
通常来说,异步计算的过程对于调用者是不可见的,原因有两点:第一、每一个GPU设备以操作序列进入设备的顺序来执行这个操作序列;第二、当数据在CPU和GPU之间或者GPU之间进行拷贝时,PyTorch会自动执行必要的同步操作。于是,计算过程的外在表现就是仿佛每一个操作都在同步执行。
使用者可以通过设置环境变量CUDA_LAUNCH_BLOCKING=1
来强制使用同步计算。当一个error
在GPU上发生时,可以手动设置来排错。
异步计算的一个后果就是当没有进行时间同步时,时间的测量(耗时测量)就会不准确。为了得到精确的时间测量,要么在测量前调用torch.cuda.synchronize()
,要么使用torch.cuda.Event
来记录时间,详细代码示例如下:
# 使用torch.cuda.Event的方式
start_event = torch.cuda.Event(enable_timing=True)
end_event = torch.cuda.Event(enable_time=True)
start_event.record()
# DO SOMETHING...
end_event.record()
torch.cuda.synchronize() # 等待事件被记录
elapsed_time_ms = start_event.elapsed_time(end_time)
作为一个例外(As an exception),一些形如to()
和copy()
的函数允许显示的non_blocking
参数,这样可以使得调用者避开(bypass)不必要时刻的同步操作。另外的例外情况就是CUDA streams
。
一个CUDA stream
是一个相信执行序列,该序列属于特定的GPU设备。通常不需要显示创建,因为每一个GPU设备默认情况下会创建一个自己的stream
。
stream
的操作序列以特们被创建的顺序在stream
中序列化好,但是来自不同streams
的操作序列可以以一个相对顺序并发执行,除非显示调用了synchronize()
或者wait_stream()
等同步函数的情况下不能并发执行。以下举出流使用的示例代码:
cuda = torch.device('cuda')
s = torch.cuda.Stream() # 创建流对象
A = torch.empty((100,100),device=cuda).normal_(0.0,1.0)
with torch.cuda.streams(s):
# sum()函数可能在normal_()函数结束之前就已经开始执行了,体现了并发执行
B = torch.sum(A)
当当前的stream
是默认的stream
,当发生数据移动时,PyTorch
会自动执行一些必要的同步操作。然而,当没有使用默认的stream
时,则需要调用者来确保正常的同步过程。
PyTorch
会使用缓存内存分配器(caching memory allocator
)来加速内存分配。使用该机制可以使得在内存回收时速度更快,因为这过程没有GPU设备的同步过程。但是,被allocator
管理的未使用的内存在nvidia-smi
上还会显示这部分内存仍被占用。使用者可以使用memory_allocated()
和max_memory_allocated()
来对tensors
占用的内存进行监控,可以使用memory_reserved()
和max_memory_reserved()
来监控caching allocator
管理的内存数量。可以调用empty_cache()
来释放所有没有使用的cached memory
,这样这部分内存就可以被其他的GPU应用使用了。被tensors
占用的GPU内存不会被释放因此可以使用的GPU内存的数量不会增加,除非把这部分内存显式释放掉。
对于更多的advanced users
,Pytorch
提供了memory_stats()
来监控内存数量,提供了memory_snapshot()
函数来捕获内存分配状态。可以通过这两个函数来查看自己的代码对于内存的使用情况。
对于每一个CUDA
设备,一个关于cuFFT
计划的LRU算法被用来加速在CUDA tensor
上运行的FFT方法,这些tensors
具有相同的形状和配置。由于一些cuFFT
计划可能分配GPU内存,这些caches
有一个最大容量。
可以通过以下API参数来查询当前设备的cache
的配置情况:
torch.backends.cuda.cufft_plan_cache.max_size
:给定cache
的容量(在CUDA 10 及以上版本是4096,老版本是1023)。通过设置该属性可以修改容量torch.backends.cuda.cufft_plan_cache.size
:当前cache
中仍存在的plans
的数量torch.backends.cuda.cufft_plan_cache.clear()
:清空cache
为了在non-default
设备上控制和查询plan caches
,可以通过torch.device
或者GPU设备的索引来索引torch.backends.cuda.cufft_plan_cache
对象,并访问以上描述到的三个属性。举例如下:
torch.backends.cuda.cufft.plan_cache[1].max_size=10 # 设置GPU设备1的容量为10
由于Pytorch
的结构,使用者需要显示写设备不可知的代码。第一步是决定是都使用GPU。一个通用的做法是使用Python
的argparse
模块来读取用户参数,结合使用is_available()
,使用一个标记来不适用CUDA。
import argparse
import torch
parse = argparse.ArgumentParser(description='Pytorch example')
parse.add_argument('--disable-cuda',action='store_true',help='DisableCUDA')
args = parser.parse_args()
args.device = None
if not args.disable_cuda and torch.cuda.is_available():
args.device = torch.device('cuda')
else:
args.device = torch.device('cpu')
# 在设备上创建tensor和神经网络(根据参数才知道设备是CPU还是GPU)
x = torch.empty((8,42),device=args.device)
net = Network().to(device=args.device)
使用数据加载器(dataloader
)加载数据:
cuda0 = torch.device('cuda:0')
for i,x in enumerate(train_loader):
x = x.to(cuda0)
使用torch.cuda.device
来使用控制 tensor创建时所在的GPU设备
print("Outside device is 0")
with torch.cuda.device(1): # 选择GPU设备1
print("Inside device is 1")
print("Outside device is still 0")
当存在一个tensor
前提下,想要在相同的GPU设备上创建一个新的tensor
,此时可以使用torch.Tensor.new_*
这一类的方法。torch.*
类的方法使用需要当前GPU的context
信息和传入的属性参数,torch.Tensor.new_*
方法保留设备信息和tensor
的其他属性。
以下代码展示了在参数的前向传播过程中模型创建时,新的tensors
在其内部创建的过程。
cuda = torch.device('cuda')
x_cpu = torch.empty(2)
x_gpu = torch.empty(2,device=cuda)
x_cpu_long = torch.empty(2,dtype=torch.int64)
# 在CPU(因为x_cpu是CPU上的Tensor)上使用new_full创建一个tensor,元素值为0.3
y_cpu = x_cpu.new_full([3,2],fill_value=0.3)
print(y_cpu)
# 在GPU(因为x_gpu是GPU上的Tensor)上使用new_full创建一个tensor,元素值为-5
y_gpu = x_gpu.new_full([3,2],fill_value=-5)
print(y_gpu)
# 在CPU上创建Tensor
y_cpu_long = x_cpu_long.new_tensor([[1,2,3]])
print(y_cpu_long)
输出结果如下:
tensor([[ 0.3000, 0.3000],
[ 0.3000, 0.3000],
[ 0.3000, 0.3000]])
tensor([[-5.0000, -5.0000],
[-5.0000, -5.0000],
[-5.0000, -5.0000]], device='cuda:0')
tensor([[ 1, 2, 3]])
tensor的new_full函数说明如下:
创建同类型和同大小的tensor:ones_like()
或者zeros_like()
x_cpu = torch.empty(2,3)
x_gpu = torch.empty(2,3)
y_cpu = torch.ones_like(x_cpu)
y_gpu = torch.zeros_like(x_gpu)
当来自固定(页面锁定)内存时,主机到GPU的内存拷贝要更快。CPU中的tensors
和storages
向外暴露pin_memory()
方法,然后返回一个对象的拷贝,数据放在锁住区域(pinned region
)。
一旦对一个tensor
或者storage
进行锁定,则可以使用异步GPU拷贝操作。只需要将参数non_blocking=True
传递到to()
或者cuda()
方法中即可。这样可以使得数据传输和计算过程一起进行。
可以将pin_memory=True
传递到DataLoader
的构造器中来返回批量数据,这部分数据被放置在锁住内存(pinned memory
)中。
许多涉及到批量数据处理和多块GPU的案例应该默认使用DataParallel
来利用多块GPU。甚至使用GIL,一个Python
的单进程程序可以渗入到多块GPU中。