Shortcuts

자동 혼합 정밀도(Automatic Mixed Precision) 가이드

저자: Michael Carilli 역자: 오왕택

torch.cuda.amp 에서는 혼합 정밀도(mixed precision)를 위해 편리한 메소드를 제공합니다. 이 방식에서는 일부 연산이 torch.float32 (float) 데이터 타입을 사용하고, 다른 연산은 torch.float16 (half) 을 사용합니다. 예를 들어, 선형 계층이나 합성곱 같은 연산은 float16 또는 bfloat16 에서 훨씬 빠르게 실행됩니다. 반면, 합계(sum), 평균(mean), 최대/최소값 연산과 같은 reduction 연산은 값의 범위 변동이 크므로 더 넓은 동적 범위를 제공하는 float32 가 필요합니다. 혼합 정밀도는 각 연산에 적합한 데이터 타입을 매칭하여, 네트워크의 실행 시간과 메모리 사용량을 줄이는 데 도움을 줍니다.

일반적으로 《자동 혼합 정밀도 학습》은 torch.autocasttorch.cuda.amp.GradScaler 를 함께 사용합니다.

이 가이드에서는 기본 정밀도에서 간단한 네트워크의 성능을 측정한 후, autocastGradScaler 를 추가하여 동일한 신경망을 혼합 정밀도로 실행해 성능을 향상시키는 과정을 설명합니다.

이 가이드는 파이썬 스크립트로 다운로드하여 실행할 수 있습니다. 필요한 요구 사항은 PyTorch 1.6 이상과 CUDA를 지원하는 GPU입니다.

혼합 정밀도는 주로 Tensor Core가 지원되는 아키텍처(Volta, Turing, Ampere)에서 좋은 성능을 냅니다. 이러한 아키텍처에서는 2~3배의 성능 향상이 나타날 수 있습니다. 이전 아키텍처(Kepler, Maxwell, Pascal)에서는 약간의 성능 향상이 있을 수 있습니다. GPU의 아키텍처를 확인하려면 nvidia-smi 명령을 실행하세요.

import torch, time, gc

# 시간 처리에 사용할 함수들
start_time = None

def start_timer():
    global start_time
    gc.collect()
    torch.cuda.empty_cache()
    torch.cuda.reset_max_memory_allocated()
    torch.cuda.synchronize()
    start_time = time.time()

def end_timer_and_print(local_msg):
    torch.cuda.synchronize()
    end_time = time.time()
    print("\n" + local_msg)
    print("Total execution time = {:.3f} sec".format(end_time - start_time))
    print("Max memory used by tensors = {} bytes".format(torch.cuda.max_memory_allocated()))

간단한 신경망

다음과 같은 선형 계층과 ReLU 연산의 연속은 혼합 정밀도를 사용했을 때 성능 향상을 보여줄 수 있습니다.

def make_model(in_size, out_size, num_layers):
    layers = []
    for _ in range(num_layers - 1):
        layers.append(torch.nn.Linear(in_size, in_size))
        layers.append(torch.nn.ReLU())
    layers.append(torch.nn.Linear(in_size, out_size))
    return torch.nn.Sequential(*tuple(layers)).cuda()

batch_size, in_size, out_size, 그리고 num_layers 는 GPU에 충분한 작업량을 부여하기 위해 크게 설정했습니다. 일반적으로 GPU가 충분히 사용될 때 혼합 정밀도가 가장 큰 성능 향상을 제공합니다. 작은 신경망은 CPU에 의해 제약을 받을 수 있으며, 이 경우 혼합 정밀도는 성능을 향상시키지 못할 수 있습니다. 또한, 선형 계층에서 사용되는 차원들은 8의 배수로 설정되어 Tensor Core를 지원하는 GPU에서 Tensor Core를 사용할 수 있게 구성되었습니다. (아래 Troubleshooting 해결 참조)

연습 문제: 사용할 사이즈를 다양하게 설정하여 혼합 정밀도 사용 시 성능 향상이 어떻게 달라지는지 확인해 보세요.

batch_size = 512 # 128, 256, 513도 시도해보세요.
in_size = 4096
out_size = 4096
num_layers = 3
num_batches = 50
epochs = 3

device = 'cuda' if torch.cuda.is_available() else 'cpu'
torch.set_default_device(device)

# 기본 정밀도로 데이터를 생성합니다.
# 아래에서 기본 정밀도와 혼합 정밀도 실험 모두에서 동일한 데이터를 사용합니다.
# 혼합 정밀도를 활성화할 때 입력의 ``dtype`` 을 수동으로 변경할 필요는 없습니다.
data = [torch.randn(batch_size, in_size) for _ in range(num_batches)]
targets = [torch.randn(batch_size, out_size) for _ in range(num_batches)]

loss_fn = torch.nn.MSELoss().cuda()

기본 정밀도

torch.cuda.amp 없이, 아래의 간단한 신경망은 모든 연산을 기본 정밀도 (torch.float32) 로 실행합니다.

net = make_model(in_size, out_size, num_layers)
opt = torch.optim.SGD(net.parameters(), lr=0.001)

start_timer()
for epoch in range(epochs):
    for input, target in zip(data, targets):
        output = net(input)
        loss = loss_fn(output, target)
        loss.backward()
        opt.step()
        opt.zero_grad() # 여기서 set_to_none=True는 성능을 약간 향상시킬 수 있습니다.
end_timer_and_print("Default precision:")

torch.autocast 추가하는 방법

torch.autocast 의 인스턴스는 스크립트의 일부 영역을 혼합 정밀도로 실행할 수 있도록, 컨텍스트 관리자로 작동합니다. 이 영역에서 CUDA 연산은 성능을 개선하면서 정확도를 유지하기 위해 autocast 가 선택한 dtype 으로 실행됩니다. 각 연산에 대해 autocast 가 선택하는 정밀도와 해당 정밀도를 선택하는 상황에 대한 자세한 내용은 Autocast Op Reference 를 참조하세요.

for epoch in range(0): # 0 epoch으로 설정한 것은 이 섹션을 설명하기 위한 것입니다.
    for input, target in zip(data, targets):
        # ``autocast`` 아래에서 순전파를 실행합니다
        with torch.autocast(device_type=device, dtype=torch.float16):
            output = net(input)
            # output은 float16입니다. 이는 선형 계층이 ``autocast`` 에 의해 float16으로 변환되기 때문입니다.
            assert output.dtype is torch.float16

            loss = loss_fn(output, target)
            # loss는 float32입니다. 이는 ``mse_loss`` 계층이 ``autocast`` 에 의해 float32로 변환되기 때문입니다.
            assert loss.dtype is torch.float32

        # ``autocast`` 에서 backward() 전에 종료합니다.
        # ``autocast`` 에서 역전파를 실행하는 것은 권장되지 않습니다.
        # 역전파 연산은 해당 순전파 연산을 위해 ``autocast`` 가 선택한 것과 동일한 ``dtype`` 으로 실행됩니다.
        loss.backward()
        opt.step()
        opt.zero_grad() # 여기서 set_to_none=True는 성능을 약간 향상시킬 수 있습니다.

GradScaler 추가하는 방법

Gradient scaling 은 혼합 정밀도로 학습할 때 작은 크기의 변화도가 0으로 사라지는 (《underflowing》) 하는 것을 방지하는 데 도움을 줍니다. torch.cuda.amp.GradScaler 는 변화도 스케일링 단계를 편리하게 수행합니다.

# 수렴 실행의 시작에서 기본 인자를 사용하여 한 번 ``scaler`` 를 생성합니다.
# 신경망이 기본 ``GradScaler`` 인수로 수렴하지 않는다면, 이슈를 제출해주세요.
# 수렴 실행 전체에서 동일한 ``GradScaler`` 인스턴스를 사용해야 합니다.
# 동일한 스크립트에서 여러 번의 수렴 실행을 수행할 경우, 각 실행은 전용의 새로운 ``GradScaler`` 인스턴스를 사용해야 합니다.
# ``GradScaler`` 인스턴스는 가볍습니다.
scaler = torch.cuda.amp.GradScaler()

for epoch in range(0): # 0 epoch으로 설정한 것은 이 섹션을 설명하기 위한 것입니다.
    for input, target in zip(data, targets):
        with torch.autocast(device_type=device, dtype=torch.float16):
            output = net(input)
            loss = loss_fn(output, target)

        # 손실을 조정합니다. 조정된 손실에 대해 ``backward()`` 를 호출하여 조정된 변화도를 생성합니다.
        scaler.scale(loss).backward()

        # ``scaler.step()`` 은 먼저 옵티마이저에 할당된 매개변수의 변화도를 복원합니다.
        # 이 변화도에 ``inf`` 나 ``NaN`` 이 포함되어 있지 않으면 optimizer.step()이 호출됩니다.
        # 그렇지 않으면 optimizer.step()을 건너뜁니다.
        scaler.step(opt)

        # 다음 반복을 위해 조정된 값을 업데이트합니다.
        scaler.update()

        opt.zero_grad() # 여기서 set_to_none=True는 성능을 약간 향상시킬 수 있습니다.

《Automatic Mixed Precision》 소개한 것들 같이 사용하는 방법

(다음 예시는 autocastGradScaler 을 사용할 수 있는 편리한 인자인 enabled 를 보여줍니다. 만약 False로 설정되면, autocastGradScaler 의 호출이 무효화됩니다. 이를 통해 if/else 문 없이 기본 정밀도와 혼합 정밀도 간 전환이 가능합니다.)

use_amp = True

net = make_model(in_size, out_size, num_layers)
opt = torch.optim.SGD(net.parameters(), lr=0.001)
scaler = torch.cuda.amp.GradScaler(enabled=use_amp)

start_timer()
for epoch in range(epochs):
    for input, target in zip(data, targets):
        with torch.autocast(device_type=device, dtype=torch.float16, enabled=use_amp):
            output = net(input)
            loss = loss_fn(output, target)
        scaler.scale(loss).backward()
        scaler.step(opt)
        scaler.update()
        opt.zero_grad() # 여기서 set_to_none=True는 성능을 약간 향상시킬 수 있습니다.
end_timer_and_print("Mixed precision:")

변화도 확인/수정하기 (예: 클리핑)

scaler.scale(loss).backward() 로 생성된 모든 변화도는 조정됩니다. 만약 backward()scaler.step(optimizer) 사이에서 파라미터의 .grad 속성을 수정하거나 확인하고 싶다면, 먼저 scaler.unscale_(optimizer) 를 사용하여 변화도를 복원해야 합니다.

for epoch in range(0): # 0 epoch으로 설정한 것은 이 섹션을 설명하기 위한 것입니다.
    for input, target in zip(data, targets):
        with torch.autocast(device_type=device, dtype=torch.float16):
            output = net(input)
            loss = loss_fn(output, target)
        scaler.scale(loss).backward()

        # 옵티마이저에 할당된 파라미터의 변화도를 제자리에서 복원합니다.
        scaler.unscale_(opt)

        # 옵티마이저에 할당된 파라미터의 변화도가 이제 복원되었으므로, 평소와 같이 클리핑할 수 있습니다.
        # 이때 클리핑에 사용하는 max_norm 값은 변화도 조정이 없을 때와 동일하게 사용할 수 있습니다.
        torch.nn.utils.clip_grad_norm_(net.parameters(), max_norm=0.1)

        scaler.step(opt)
        scaler.update()
        opt.zero_grad() # 여기서 set_to_none=True는 성능을 약간 향상시킬 수 있습니다.

저장/재개하는 법

Amp가 활성화 상태에서 비트 단위의 정확도로 저장/재개하려면, scaler.state_dictscaler.load_state_dict 를 사용하세요.

저장할 때는, 일반적인 모델과 옵티마이저의 상태와 함께 scaler 의 상태도 저장해야 합니다. 이를 각 반복하는 시작 시점, 즉 어떤 순전파 전에 하거나, 반복이 끝난 후에 scaler.update() 이후에 수행하면 됩니다.

checkpoint = {"model": net.state_dict(),
              "optimizer": opt.state_dict(),
              "scaler": scaler.state_dict()}
# 예를 들어 체크포인트를 작성하려면,
# torch.save(checkpoint, "filename")

재개할 때는, 모델과 옵티마이저의 상태와 함께 scaler 의 상태도 로드합니다. 예를 들어 체크포인트를 읽으려면,

dev = torch.cuda.current_device()
checkpoint = torch.load("filename",
                        map_location = lambda storage, loc: storage.cuda(dev))
net.load_state_dict(checkpoint["model"])
opt.load_state_dict(checkpoint["optimizer"])
scaler.load_state_dict(checkpoint["scaler"])

체크포인트가 Amp 없이 생성된 경우, Amp를 사용하여 훈련을 재개하고 싶다면, 모델과 옵티마이저 상태를 평소처럼 체크포인트에서 로드합니다. 이 체크포인트에는 저장된 scaler 상태가 없으므로 새로운 GradScaler 인스턴스를 사용해야 합니다.

반대로 체크포인트가 Amp로 생성된 경우, Amp 를 사용하지 않고 훈련을 재개하려면, 모델과 옵티마이저 상태를 체크포인트에서 평소처럼 로드하고, 저장된 scaler 상태는 무시하면 됩니다.

추론/평가

autocast 는 단독으로 사용하여 추론 또는 평가의 순전파를 감쌀 수 있습니다. 이 경우 GradScaler 는 필요하지 않습니다.

고급 주제

고급 사용 사례에 대한 내용은 Automatic Mixed Precision Examples 를 참조하세요. 예시에는 다음과 같은 내용이 포함되어 있습니다:

  • 변화도 축적

  • 변화도 페널티/이중 역전파

  • 다중 모델, 옵티마이저 또는 손실을 사용하는 신경망

  • 다중 GPU (torch.nn.DataParallel 또는 torch.nn.parallel.DistributedDataParallel)

  • 사용자 정의 autograd 함수 (torch.autograd.Function 의 서브클래스)

동일한 스크립트에서 여러 번의 수렴 실행을 수행하는 경우, 각 실행은 새로운 GradScaler 인스턴스를 사용해야 합니다. GradScaler 인스턴스는 가볍습니다.

만약 사용자 정의 C++ 연산을 디스패처에 등록하려면, 디스패처 튜토리얼의 autocast section 을 참조하세요.

Troubleshooting 해결

Amp를 사용한 속도 향상이 미미한 경우

  1. 네트워크가 GPU(들)에 충분한 작업을 제공하지 못하고 있어, CPU에 의한 병목 현상이 발생할 수 있습니다. 이 경우 Amp의 GPU 성능에 대한 효과는 중요하지 않을 수 있습니다.

    • GPU를 포화 상태로 만들기 위한 대략적인 방법은, 메모리 부족(OOM)이 발생하지 않는 선에서 가능한 한 배치 크기나 네트워크 크기를 늘리는 것입니다.

    • 과도한 CPU-GPU 동기화(예: .item() 호출이나 CUDA 텐서에서 값을 출력하는 것)를 피해야 합니다.

    • 사소한데 많은 CUDA 연산들을 피하고, 가능한 한 이러한 연산들을 몇 개의 큰 CUDA 연산으로 합치는 것이 좋습니다.

  2. 신경망이 GPU 계산 병목 현상을 겪고 있을 수 있지만 (많은 matmul 또는 합성곱 연산), 사용 중인 GPU에 텐서 코어가 없을 수 있습니다. 이 경우 속도 향상이 적을 수 있습니다.

  3. matmul 차원이 Tensor Core에 친화적이지 않을 수 있습니다. matmul 에 참여하는 사이즈가 8의 배수인지 확인해야 합니다. (인코더/디코더가 있는 NLP 모델의 경우, 이 점이 미묘할 수 있습니다. 또한, 합성곱 연산도 Tensor Core 사용을 위해 유사한 크기 제약을 가졌었지만, CuDNN 7.3 버전 이후로는 이러한 제약이 없습니다. 자세한 내용은 여기 에서 확인할 수 있습니다.)

손실이 inf/NaN인 경우

먼저 신경망이 고급 사용 사례 에 해당하는지 확인하세요. 또한 Prefer binary_cross_entropy_with_logits over binary_cross_entropy 을 참조하세요.

Amp 사용이 올바르다고 확신한다면, 이슈를 제기해야 할 수도 있지만, 그 전에 다음 정보를 수집하는 것이 도움이 될 수 있습니다:

  1. autocast 또는 GradScaler 를 각각 비활성화 (enabled=False 를 생성자에 전달)하고 infNaN 이 여전히 발생하는지 확인합니다.

  2. 신경망의 일부(예: 복잡한 손실 함수)가 오버플로우되는 것이 의심된다면, 해당 순전파 영역을 float32 로 실행하고 infNaN 이 발생하는지 확인하세요. The autocast docstring 의 마지막 코드 단락에서 autocast 를 로컬로 비활성화하고 하위 영역의 입력을 캐스팅하여 하위 영역을 float32 로 실행하는 방법을 확인할 수 있습니다.

타입 불일치 오류 (CUDNN_STATUS_BAD_PARAM 으로 나타날 수 있음)

Autocast 는 캐스팅으로 이득을 얻거나 필요한 연산들을 모두 처리하려고 합니다. 명시적으로 다루어진 연산들 은 수치적 특성뿐만 아니라 경험에 기반하여 선택되었습니다. Autocast 가 활성화된 순전파 영역이나 그 영역을 따른 역전파에서 타입 불일치 오류가 발생한다면, autocast 가 어떤 연산을 놓쳤을 가능성이 있습니다. 오류 추적내용과 함께 이슈를 제기하세요. 세부 정보 제공을 위해 스크립트를 실행하기 전에 export TORCH_SHOW_CPP_STACKTRACES=1 을 설정하여 어느 백엔드 연산에서 실패하는지 확인할 수 있습니다.

Total running time of the script: ( 0 minutes 0.000 seconds)

Gallery generated by Sphinx-Gallery


더 궁금하시거나 개선할 내용이 있으신가요? 커뮤니티에 참여해보세요!


이 튜토리얼이 어떠셨나요? 평가해주시면 이후 개선에 참고하겠습니다! :)

© Copyright 2018-2024, PyTorch & 파이토치 한국 사용자 모임(PyTorch Korea User Group).

Built with Sphinx using a theme provided by Read the Docs.

PyTorchKorea @ GitHub

파이토치 한국 사용자 모임을 GitHub에서 만나보세요.

GitHub로 이동

한국어 튜토리얼

한국어로 번역 중인 PyTorch 튜토리얼입니다.

튜토리얼로 이동

커뮤니티

다른 사용자들과 의견을 나누고, 도와주세요!

커뮤니티로 이동