본문으로 바로가기

Pytorch docs 요약] Cuda semantics

category Programming/Pytorch 2019. 2. 2. 19:22

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는 비동기적으로 동작한다. 다만 사용자는 그렇게 보이지 않는다. 그 이유로

  1. 각 장치는 queue로 들어가 순서대로 동작한다.
  2. PyTorch가 자동적으로 장치 간에 복사할 때 동기화를 해주어 동기적으로 계산이 되는 것처럼 해준다.

물론 강제로 동기적으로 동작하게 할 수도 있다.

  1. 환경 변수 CUDA_LAUNCH_BLOCKING=1로 설정하기
  2. to()copy()같은 함수에서 non_blocking 매개변수를 사용하기
  3. 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_likezeros_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