CUDA 시맨틱스

CUDA 시맨틱스 (CUDA semantics)

GPU 연산을 처음 시도할 때 흔히 겪는 게 "텐서가 어느 GPU에 있는지"에 따른 혼란이에요. torch.cuda 는 현재 선택된 GPU를 추적하고, 기본적으로 모든 CUDA 텐서가 그 장치에 만들어지도록 관리해요. 장치(device)를 명확히 다루는 습관만 잡으면 멀티 GPU 코드도 예측 가능해져요.

출처: PyTorch 공식 문서 — CUDA semantics

장치 지정과 텐서 이동

현재 장치는 torch.cuda.device 컨텍스트 매니저로 바꿀 수 있어요. torch.device('cuda:0') 같은 방식으로 특정 GPU를 가리킬 수 있고, tensor.cuda()tensor.to(device=...) 로 CPU 텐서를 GPU로 옮길 수 있어요.

cuda = torch.device('cuda')     # 기본 CUDA 장치
cuda0 = torch.device('cuda:0')
cuda2 = torch.device('cuda:2')  # GPU 2 (0부터 시작)

x = torch.tensor([1., 2.], device=cuda0)
y = torch.tensor([1., 2.]).cuda()

with torch.cuda.device(1):
    # GPU 1에 텐서 할당
    a = torch.tensor([1., 2.], device=cuda)
    b = torch.tensor([1., 2.]).cuda()
    # Tensor.to로도 전송 가능
    b2 = torch.tensor([1., 2.]).to(device=cuda)
    c = a + b

장치 간 연산 제약

기본적으로 copy_()to(), cuda() 같은 복사 계열을 제외한 서로 다른 GPU 간 연산은 허용되지 않아요. 피어 투 피어(peer-to-peer) 메모리 접근을 켜지 않으면 서로 다른 장치에 걸친 연산을 시도할 때 오류가 나요.

비동기 실행과 스트림

CUDA 연산은 비동기로 실행되는 경우가 많고, torch.cuda.Stream 으로 실행 순서를 제어할 수 있어요. 역전파 스트림 시맨틱스도 알아 두면 좋은데, 예컨대 기존 버전(1.9 이전)은 기본 스트림을 항상 동기화했지만 이후 동작이 달라졌어요.

with torch.cuda.stream(s):
    loss.backward()
# grads 사용

장치 비의존(device-agnostic) 코드 작성

args.device 같은 값을 받아 두고, 텐서를 만들 때마다 그 장치를 넘기면 CPU/GPU 양쪽에서 같은 코드가 돌아가요. 시스템에 여러 GPU가 있을 때는 CUDA_VISIBLE_DEVICES 환경 변수로 PyTorch가 볼 수 있는 GPU를 제한할 수 있어요.

x = torch.empty((8, 42), device=args.device)
net = Network().to(device=args.device)

같은 텐서의 타입·장치를 유지한 채 새 텐서를 만드려면 torch.Tensor.new_* 메서드를 쓰는 게 좋아요. 팩토리 함수는 현재 GPU 컨텍스트에 영향을 받지만, new_* 메서드는 기존 텐서의 장치·속성을 보존해요. ones_like()zeros_like() 역시 장치와 dtype 을 그대로 유지하는 편의 함수예요.

고정(pinned) 메모리 사용

호스트에서 GPU로의 복사는 pinned(page-locked) 메모리에서 시작할 때 훨씬 빨라요. CPU 텐서·저장소는 pin_memory() 메서드로 데이터를 pinned 영역에 넣은 복사본을 만들 수 있어요.

CUDA Graphs

make_graphed_callables() 가 돌려주는 callable은 기본적으로 autograd를 인지해서, 학습 루프에서 전달한 함수나 nn.Module 을 그대로 대체해 쓸 수 있어요. 전체 네트워크가 아닌 일부만 캡처하는 partial-network capture 같은 기법으로 커널 실행 오버헤드를 줄이는 데 유용해요.

더 알아보기