Search

[작성중]Pytorch에서 Mixed Precision 사용하기

이전 블로그에서 작성했던 글
Pytorch 공식
torch.cuda.amp는 mixed precision을 위한 편리한 기능을 제공하여 준다
몇몇 연산은 torch.float32(float) datatype을 사용하고, 다른 연산들은 torch.float16(half) 연산을 사용하도록
사진을 보면 Conv 연산 등은 half로, Softmax나 Loss같은 acc 유지에 중요한 역할을 하는 부분은 FP32로 유지
weight는 single precision을 유지하고 back propagation은 half precision으로 수행
Mixed precisoin은 각 연산에 그것에 적절한 datatype을 적용하도록 하고 네트워크의 runtime과 memory footprint를 감소시켜준다
Pytorch에서 automatic mixed precision training은 일반적으로 torch.cuda.amp.autocasttorch.cuda.amp.GradScaler을 함께 씀을 의미한다
torch.cuda.amp.autocast : 선택한 영역에서 autocasting을 가능하게 한다. autocasting은 자동으로 acc는 유지하면서 performance는 향상킬 GPU연산을 위한 precision을 선택한다.
torch.cuda.amp.GradScaler : 편리하게 gradient scaling의 step을 수행하는 것을 돕는다. 이는 float16 fradients로 gradient underflow를 감소시킴으로써 네트워크의 수렴을 향상시킨다. forwardpass에서 특정 연산이 float 16이라면 이 연산을 위한 backward pass는 float16 gradient를 가질 것이다. 매우 작은 값의 gradient 값은 float16에서 충분하게 표현되지 않기 때문에 underflow가 발생할 수 있다. 따라서 연관 파라미터의 업데이트가 이뤄지지 않는다. 이를 방지하기 위해 gradient scaling이 네트워크의 loss를 scale factor에 의해 곱하고 backward pass를 scaled loss에서 수행한다. backward pass를 흐르는 gradient들은 이후 다시 같은 factor 로 scale된다. 다시말해, gradient값들은 더 큰 값을 갖게되어 zero로 flush되지 않는다.  각 파라미터의 gradients들은 ( .grad ) optimizer 업데이트 이전에 unscaled되어야한다. 그래서 scale factor가 learning rate에 영향을 미치지 않도록 해야한다.
Underflow와 Overflow
Mixed Precision은 주로 Tensor Core-enabled architectures ( Volta, Turing, Ampere ) 에서 이점이 있다 ( Tensor core의 연산 자체가 FP16 ). 보통 2-3배 빨라질 것이다
이전의 아키텍쳐들 ( Kepler, Maxwell, Pascal ) 은 큰 속도 향상은 없을 수도 있다.

일반적인 Mixed Precision Training

import torch import torch.nn as nn from torch.cuda.amp import autocast, GradScaler # 모델과 optimizers는 default precision으로 생성 model = Net().cuda() optimizer = torch.optim.SGD(model.parameters(), ... ) loss_fn = nn.L1Loss() # training 시작 시에 GradScaler 생성 ( 한번 ) scaler = GradScaler() for epoch in epochs: for input, target in data: optimizer.zero_grad() # optmizer gradient 초기화 with autocast(): output = model(input) assert output.dtype is torch.float16 # check loss = loss_fn(output, target) ## Loss를 scale한다 # backward()를 scaled loss에 수행해 scaled gradients를 생성한다 # backward pass를 autocast 하에 하는 것은 지향하지 않는다 # Backward 연산은 대응하는 forward 연산대해 선택한 동일한 dtype autocast에서 실행된다 scaler.scale(loss).backward() ## scaler.step()은 # 첫번쨰로 optimizer에 할당된 파라미터들의 gradients를 unscale하고 # gradients가 infs 나 NaNs를 포함하지 않는다면 optimizer.step()이 호출된다. # infs나 NaNs가 포함되어 있다며너 optimizer.step()는 건너뛰어진다 scaler.step(optimizer) ## scaler.update()는 # 다음 iteration을 위한 scale이 업데이트 된다 scaler.update()
Python
복사

추가 예시

import torch, time, gc # Timing utilities 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())) 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.cuda.amp.autocast(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 here can modestly improve performance end_timer_and_print("Mixed precision:")
Python
복사

Working with Unscaled Gradients

scaler.scale(loss).backward() 에서 생성되는 loss들은 모두 scale된다.
backward()와 scaler.step(optimizer) 사이에 gradients들을 변형하거나 검사하고 싶다면 unscale을 먼저 해야한다.
예를 들어 gradient clipping(torch.nn.utils.clip_grad_norm_()) 같은 경우엔 global norm 이나 maximum magnitude로 gradient집합을 조작한다
unscale없이 이를 조정한다면 gradient의 norm 이나 maximum 도 같이 scale되어야 하고, 개발자가 요청한 threshold가 invalid하게 된다
scaler.unscale_(optimizer) : optimizer에 할당되어 있는 gradient들을 unscale

Gradient Clipping

scaler = GradScaler() for epoch in epochs: for input, target in data: optimizer.zero_grad() with autocast(): output = model(input) loss = loss_fn(output, target) scaler.scale(loss).backward() # Unscales the gradients of optimizer's assigned params in-place scaler.unscale_(optimizer) # Since the gradients of optimizer's assigned params are unscaled, clips as usual: torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm) # optimizer's gradients are already unscaled, so scaler.step does not unscale them, # although it still skips optimizer.step() if the gradients contain infs or NaNs. scaler.step(optimizer) # Updates the scale for next iteration. scaler.update()
Python
복사
scaler는 scaler.unscale_(optimizer)가 호출되었다는 것을 기록한다 ( 이 iteration에서 이 optimizer에 호출되었다는 것을)
따라서 scaler.step(optimizer)는 optimizer.step() 호출 전에 쓸데없이 unscale을 수행하지 않는다. ( 원래 scaler.step의 첫 순서가 unscale )

Working with Scaled Gradients

Gradient accumulation

gradient accumulation은