CUDA SEMANTICS
torch.cuda
는 CUDA operations들을 설정하고 실행할 수 있게 해준다. 선택한 GPU를 추적하고 할당한 모든 CUDA tensor는 ‘torch.cuda.device’ 같이 명시하지 않는한 default GPU에 할당된다.
일단 할당되면 계속 그 GPU에서 처리 되기 때문에 copy_()
나 to()
, cuda()
로 따로 복사해서 옮기지 않는 한 해당 GPU에서만 동작한다.
예시
cuda = torch.device('cuda') # 기본 CUDA 장치에 할당
cuda0 = torch.device('cuda:0') # 보통 0부터 시작
cuda2 = torch.device('cuda:2') # GPU 2에 할당
x = torch.tensor([1., 2.], device=cuda0)
# x.device 는 device(type='cuda', index=0)
y = torch.tensor([1., 2.]).cuda()
# y.device 는 device(type='cuda', index=0)
# 이 안에서는 기본적으로 GPU 1에 할당
with torch.cuda.device(1):
# GPU 1 에 할당
a = torch.tensor([1., 2.], device=cuda)
# CPU에서 GPU 1로 옮기기
b = torch.tensor([1., 2.]).cuda()
# a.device 와 b.device 둘 다 device(type='cuda', index=1)
# ``Tensor.to`` 를 통해서도 원하는 장치로 이동 가능
b2 = torch.tensor([1., 2.]).to(device=cuda)
# b.device 와 b2.device 둘 다 device(type='cuda', index=1)
c = a + b
# c.device 는 device(type='cuda', index=1)
z = x + y
# z.device 는 device(type='cuda', index=0)
# 해당 context(with절) 안에서라도 명시적으로 원하는 장치에 옮길 수 있다.
d = torch.randn(2, device=cuda2)
e = torch.randn(2).to(cuda2)
f = torch.randn(2).cuda(cuda2)
# d.device, e.device, 그리고 f.device 모두 device(type='cuda', index=2)
ASYNCHRONOUS EXECUTION
기본적으로 GPU는 비동기적으로 동작한다. 다만 사용자는 그렇게 보이지 않는다. 그 이유로
- 각 장치는 queue로 들어가 순서대로 동작한다.
- PyTorch가 자동적으로 장치 간에 복사할 때 동기화를 해주어 동기적으로 계산이 되는 것처럼 해준다.
물론 강제로 동기적으로 동작하게 할 수도 있다.
- 환경 변수
CUDA_LAUNCH_BLOCKING=1
로 설정하기 to()
와copy()
같은 함수에서non_blocking
매개변수를 사용하기- CUDA streams
CUDA streams
CUDA stream은 어떤 장치에서 실행의 흐름같은 것으로, 각 장치마다 기본적으로 default stream이 존재하므로 만들 필요는 없다.
각 stream은 만든 순서대로 직렬화(serialized)되지만, 다른 스트림의 operation은 별개의 순서를 가지고 있어서 명시적으로 동기화(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.stream(s):
# normal_() 함수가 끝나기 전에 sum() 함수를 실행할 수도 있다!
B = torch.sum(A)
자동적으로 두 스트림을 동기화해서 실행하기 떄문에 새로운 스트림을 만들어서 할 경우 잘 조절해야할 것이다...
MEMORY MANAGEMENT
PyTorch 메모리 할당 속도를 높이기 위해 caching memory allocater를 사용해서 장치간 동기화 없이 빠른 메모리 해제가 가능하다. 관리되지 않는 메모리는 nvidia-smi
에서 여전히 사용되는 것처럼 보일 수 있다. memory_allocated()
와 max_memory_allocated()
를 사용해서 모니터링할 수도 있고 empty_cache()
를 통해 사용하지 않으면서 캐시된 메모리들을 해제할 수 있다. 하지만 텐서에 의해 할당된 것은 해제 되지 않기에 주의.
BEST PRACTICES
Device-agnostic code
PyTorch 구조 때문에 CPU이든 GPU이든 장치에 상관없는 코드(device-agnostic)를 쓰고 싶은 경우가 있다. is_available()
와 argparse
모듈을 적절히 사용해서 다음과 같이 정해놓고 텐서가 CPU나 CUDA에 들어가도록 할 수 있다.
import argparse
import torch
parser = argparse.ArgumentParser(description='PyTorch Example')
parser.add_argument('--disable-cuda', action='store_true',
help='Disable CUDA')
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')
이제 args.device
를 통해 원하는 장치에 텐서를 넣을 수 있다.
x = torch.empty((8, 42), device=args.device)
net = Network().to(device=args.device)
장치가 많은 경우, 변수에 저장해놓고 사용할 수도 있을 것이다
cuda0 = torch.device('cuda:0') # CUDA GPU 0
for i, x in enumerate(train_loader):
x = x.to(cuda0)
여러 GPU를 사용하는 경우 CUDA_VISIBLE_DEVICES
환경변수를 어떤 GPU가 사용가능한지 알 수 있다. GPU를 수동으로 조절해야 하는 경우 torch.cuda.device
를 사용하는 게 제일 실용적이다.
print("Outside device is 0") # On device 0 (default in most scenarios)
with torch.cuda.device(1):
print("Inside device is 1") # On device 1
print("Outside device is still 0") # On device 0
이전의 torch.*
함수들은 현재 GPU와 넣어주는 argument에 따라 텐서가 생성되었지만(torch.Tensor참조), tensor_Tensor.new_*
함수들은 같은 device와 argument를 유지한 채 텐서를 만들 수 있다.(Creation Ops)
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)
y_cpu = x_cpu.new_full([3, 2], fill_value=0.3)
print(y_cpu)
tensor([[ 0.3000, 0.3000],
[ 0.3000, 0.3000],
[ 0.3000, 0.3000]])
y_gpu = x_gpu.new_full([3, 2], fill_value=-5)
print(y_gpu)
tensor([[-5.0000, -5.0000],
[-5.0000, -5.0000],
[-5.0000, -5.0000]], device='cuda:0')
y_cpu_long = x_cpu_long.new_tensor([[1, 2, 3]])
print(y_cpu_long)
tensor([[ 1, 2, 3]])
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)
Use pinned memory buffers
Host에서 GPU로 복사할 때 고정된(pinned, page-locked) 메모리를 사용하면 더 빠르다. pin_memory()
를 통해 고정된 영역의 데이터 복사본을 얻을 수 있다. 또한 일단 고정시키면 cuda()
에 매개변수 non_blocking=True
를 넣어줌으로써 비동기적으로 GPU 복사본을 사용할 수 있다. DataLoader
생성자에 pin_memory=True
를 넣어주어서 고정된 메모리에서 배치가 나오도록 할 수도 있다.
Use nn.DataParallel instead of multiprocessing
GPU 여러개 사용하거나 batch입력을 사용하는 대부분의 경우 DataParallel
을 기본적으로 사용한다. GIL(Python에서 쓰레드 문제가 있음)을 사용해도 하나의 Python 프로세스가 여러 GPU를 사용할 수 있다.
multiprocessing
을 사용할 떄 데이터가 요구하는 것을 잘 충족하도록 주의하라고 한다.
'Programming > Pytorch' 카테고리의 다른 글
Pytorch docs 요약] Broadcasting Semantics (0) | 2019.01.30 |
---|---|
Pytorch docs 요약] Autograd Mechanics (0) | 2019.01.27 |