[결과]: 단순히 torch.compile으로 wrapping해준 것만으로 모델 Training이 43%빠른속도로 동작했다. (다만, 이는 A100으로 측정된 결과이고, 3090같은 시리즈는 잘 동작하지 않고 심지어 더 느릴 수 있다 언급: Caveats: On a desktop-class GPU such as a NVIDIA 3090, we’ve measured that speedups are lower than on server-class GPUs such as A100. As of today, our default backend TorchInductor supports CPUs and NVIDIA Volta and Ampere GPUs. It does not (yet) support other GPUs, xPUs or older NVIDIA GPUs.)
이를 2.2버전에서는 좀 더 완성시킨 것이다!
pytorch개발자분들은 버전이 2.x로 넘어가면서 compile함수에 좀 더 집중한다하였다. 아마 점점 학습속도를 빠르게하는 면을 강화하고, 이를 점차 확대할 것 같다. (이번에 저수준커널에도 적용한 걸 보면 거의 확실시 되는듯하다.)
개발동기:
17년 시작된 이후, Eager Execution성능향상을 위해 코드 대부분을 C++로 옮기게 되었다. (Pytorch 대부분의 소스코드가 C++기반임을 근거로 알 수 있다.) (eager execution: 그래프생성없이 연산을 즉시실행하는 환경)
이런 방식을 사용자들의 코드기여도(hackability)를 낮추는 진입장벽이 되어버렸다. 이런 eager execution의 성능향상에 한계가 있다 판단하여 compiler를 만들게 되었다. 목적은 속도는 빠르게하나 pytorch experience를 해치지 않는다는 것이다.
🤔 How to use?
torch.compile()은 기존 모델에 한줄만 추가하면 된다.
import torch
import torchvision.models as models
model = models.resnet18().cuda()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
compiled_model = torch.compile(model)
x = torch.randn(16, 3, 224, 224).cuda()
optimizer.zero_grad()
out = compiled_model(x)
out.sum().backward()
optimizer.step()
compiled_model은 신경망의 forward를 좀 더 최적화시켜 속도를 빠르게 한다.
[default]: 너무 오래걸리지 않으면서 메모리를 많이 사용하지 않는 선에서 효율적인 컴파일 진행
[reduce-overhead]: 메모리를 좀 더 사용, overhead를 줄여줌
[max-autotune]: 가장 빠른 모델생성을 위해 최적화되어있다. 다만, 컴파일에 매우 오랜시간이 걸린다.
∙ dynamic:
dynamic shape에 대해 code path를 enabling할 지 결정하는 boolean 변수이다. Compiler 최적화 과정이 프로그램을 dynamic shape 프로그램에 적용될 수 없게 만드는 경우가 있는데, 이를 조절함으로써 본인이 원하는 방향대로 컴파일을 할 수 있게 해준다. 이 부분은 아직 완벽히 이해가 되지는 않지만 데이터 shape가 변하는 상황에서graph를 유동적으로 컴파일할 수 있게끔 하는 것과 관련이 있을 것 같다.
∙ fullgraph:
Numba의nopython과 유사하다. 전체 프로그램을 하나의 그래프로 컴파일하고, 만약 실패한다면 왜 불가능한지 설명해주는 error 메세지를 띄운다. 굳이 쓰지 않아도 상관없는 옵션.
∙ backend:
어떤 compiler backend를 적용할 지 결정하게 된다. 디폴트로 정해진 값은 앞서 설명했던TorchInductor가 사용되지만, 다른 옵션들도 존재한다고 한다(제대로 알아보진 않았다).
❗️ 유의점:
compile된 모델 저장 시, state_dict만 저장가능하다.
∙ 아래는 가능!
torch.save(opt_model.state_dict(), "best.pt")
torch.save(model.state_dict(), "best.pt")
torch.save(model, "best.pt")
∙ 아래는 불가능!
torch.save(opt_model, "best.pt")
Compile 이후 사용가능한 기능:
∙ TorchDynamo
Eager Mode의 가장 용이한 점: 학습도중 model_weight에 접근하거나 값을 그대로 읽어올 수 있다.
model.conv1.weight
TorchDynamo는 이를 인지하고 만약 attribute가 변한것을 감지하면 자동으로 해당부분에 대한 변화를 다시 컴파일해준다.
∙ Inference
compile함수로 compiled_model을 생성한 후 warm-up step은 초반 latency를 줄여준다. 다만, 이부분도 차차 개선시킨다 하였다.
🤔Tensorflow: ∙Google에서 개발 ∙딥러닝전용하드웨어인 TPU를 갖고 있어 GPU에서 상대적으로 자유로움
🤔Pytorch: ∙facebook의 주도하에 개발, ∙Nvidia의cuda GPU에 더욱 최적화 ∙Python First, 깔끔한 코드, numpy와의 호환성, Autograd, Dynamic Graph 등의 장점
1. Tensor
pytorch의 tensor는 numpy의배열인 ndarray와 같은 개념으로 pytorch에서 연산을 수행하기 위한 가장 기본적인 객체이다. (앞으로의모든 연산은 이 tensor 객체를 통해서 진행)
ex) numpy와 pytorch는 굉장히 비슷한 방식의 코딩스타일을 갖는다. (자료형의 경우에도 torch.uint8과 같이 표기가 가능)
import numpy as np
x = np.array([[1,2], [3,4]])
import torch
x = torch.Tensor([[1,2], [3,4]])
cf) torch.Tensor는 default로 float32를 갖는다.
2. Autograd
Autograd는자동으로 미분 및 역전파를 수행하는 기능이다. 즉, 대부분의 tensor간의 연산을 크게 신경 쓸 필요 없이 역전파알고리즘 수행 명령어를 호출할 수 있다.
이때, tensor간의 연산을 수행할 때마다 동적으로 computational graph를 생성하며, 연산의 결과물이 어떤 tensor로부터 어떤 연산을 통해 왔는지 또한 추적한다. → 결과적으로 최종 스칼라값에 역전파를 통한 미분을 수행 시, 각 tensor는 자식노드에 해당하는 tensor와 연산을 자동으로 찾아 역전파알고리즘을 계속 수행할 수 있게한다.
🤔기존 keras 및 tensorflow와 다른점?? keras와tensorflow는미리 정의한 연산들을 컴파일을 통해 고정,정해진 입력에 맞춰 tensor를 순전파시켜야 한다.
반면,Pytorch는정해진 연산이 없고 모델은 배워야 하는 parameter tensor만 미리 알고있다. 즉,가중치들이 어떠한 연산을 통해 학습 or 연산에 관여하는지 알 수 없고, 연산이 수행된 직 후 알 수 있다.
기울기를 구할 필요가 없는 연산의 경우, 다음과 같은with문법을 사용해 연산을 수행할 수 있는데, 이는prediction과 inference등을 수행할 때 유용하며, 기울기를 구하기 위한 computational graph 생성 등의 사전작업을 생략하여 연산속도 및 메모리 사용측면에서도 큰 이점이 존재한다.
with torch.no_grad():
z = (x+y) + torch.Tensor(2,2)
3. nn.Module
nn.Module 클래스는사용자가 그 위에서 필요한 모델 구조를 구현할 수 있게 해준다. nn.Module을 상속한 사용자클래스는 다시 내부에 nn.Module을 상속한 클래스객체를 선언 및 변수로 사용할 수 있다.
ex) Feed Forward 구현
import torch
import torch.nn as nn
def linear(x, W, b):
return torch.mm(W, x) + b
class MyLinear(nn.Module):
def __init__(self, input_size, output_size):
super().__init__()
self.W = torch.FloatTensor(input_size, output_size)
self.b = torch.FloatTensor(output_size)
def forward(self, x):
y = torch.mm(self.W, x) + self.b
return y
x = torch.Tensor(16, 10)
linear = MyLinear(10, 5)
y = linear(x)
x ∈ R16×10 W ∈ R10×5 b ∈ R5
>>> print([p.size() for p in linear.parameters()])
>>> [ ]
다만, 현재 parameter(W, b)의 경우, [ ]로 학습가능한 파라미터가 없다고 출력된다. 따라서 Parameter 클래스를 사용해 tensor를 감싸야 한다.
import torch
import torch.nn as nn
def linear(x, W, b):
return torch.mm(W, x) + b
class MyLinear(nn.Module):
def __init__(self, input_size, output_size):
super().__init__()
self.W = nn.Parameter(torch.FloatTensor(input_size, output_size), requires_grad=True)
self.b = nn.Parameter(torch.FloatTensor(output_size), requires_grad=True)
def forward(self, x):
y = torch.mm(self.W, x) + self.b
return y
x = torch.Tensor(16, 10)
linear = MyLinear(10, 5)
y = linear(x)
위의 경우, 출력값으로 [torch.Size([10, 5]), torch.Size([5])] 가 출력된다.
4. train()과 eval()
Backpropagation Algorithm의 경우,backward()함수를 이용해 진행가능하며, 이때 loss함수를 앞에 붙이는 형태로 표현한다.
loss.backward()
train과 eval함수는모델에 대한 training time과 inference time의 모드를 쉽게 전환할 수 있는 방법이다. nn.Module을 상속받아 구현∙생성된 객체는 기본적으로 train모드로 이를 eval()을 사용해 추론모드로 바꿀 수 있다. 이는Dropout, Batch Norm 같은 학습시와 추론 시 서로 다른 forward()동작을 하는 모듈들에도 올바르게 동작할 수 있다. 다만,추론이 끝나면 다시 train()을 선언 해 원래의 train모드로 돌아가줘야 한다.
5. GPU 사용하기
cuda()함수 ① 원하는 tensor객체를 GPU메모리에 복사하거나 ② nn.Module의 하위클래스를 GPU메모리로 이동시킬 수 있다.
x = torch.cuda.FloatTensor(16, 10)
linear = MyLinear(10, 5)
linear.cuda()
y = linear(x)
cpu() 함수 다시 PC의 메모리로 복사하거나 이동시킬 수 있다.
to()함수 tensor 또는 모듈을 원하는 device로 보낼 수 있다.
6. Pytorch에서 DNN을 수행하는 과정 요약 및 예시
①nn.Module 클래스를 상속받아 forward함수를 통해 모델구조 선언 ②해당 클래스 객체 생성 ③Optimizer 생성, 생성한 모델의 parameter를 최적화대상으로 등록 ④Data로 mini-batch를 구성, 순전파 연산그래프 생성 ⑤손실함수를 통해 최종결과값, 손실값 계산 ⑥손실에 대해서 backward() 호출 → 이를통해 연산그래프 상의 tensor들의 gradient가 채워짐 ⑦③의 optimizer에서 step()을 호출, 1 step 수행 ⑧④로 돌아가 반복
7. Pytorch 실습
[Linear Regression 분석]
📌 조건
∙ 임의로 tensor를 생성, 근사하고자하는 정답함수(wx+b)에 넣어 정답(y)을 구함
∙ 신경망 통과한 y_hat과의 차이를 구함(이때, 오류함수는 MSE Loss function을 사용)
∙ SGD를 이용해 optimization 진행
❗️ 1개의 Linear Layer를 갖는 MyModel이라는 모듈 선언
import random
import torch
import torch.nn as nn
class MyModel(nn.Module):
def __init__(self, input_size, output_size):
super(MyModel, self).__init__()
self.linear = nn.Linear(input_size, output_size)
def forward(self, x):
y = self.linear(x)
return y
batch_size = 1
epoch = 1000
iter = 10000
model = Mymodel(3, 1)
optim = torch.optim.SGD(model.parameters(), lr=0.0001, momentum=0.1)
❗️ Model과 tensor를 입력받아 순전파 후, 역전파알고리즘을 수행해 경사하강법 1 step을 진행
def train(model, x, y, optim):
# 모듈의 모든 파라미터의 기울기 초기화
optim.zero_grad()
# Feed-Forward
y_hat = model(x)
# MSE Loss
loss = ((y-y_hat)**2).sum() / x.size(0)
# BP Algorithm
loss.backward()
# GD 1 step
optim.step()
return loss.data
❗️ Train & Inference time
for epoch in range(epoch):
# train 설정
avg_loss = 0
for i in range(iter):
x = torch.rand(batch_size, 3)
y = ground_truth(x.data)
loss = train(model, x, y, optim)
avg_loss += loss
avg_loss = avg_loss / iter
# valid 설정
x_val = torch.FloatTensor([[.3, .2, .1]])
y_val = ground_truth(x_val.data)
# inference
model.eval()
y_hat = model(x_val)
model.train()
print(avg_loss, y_val.data[0], y_hat.data[0, 0])
# finish
if avg_loss < .001:
break
class Attention_Head(nn.Module):
def __init__(self, embed_dim, head_dim):
super().__init__()
self.q = nn.Linear(embed_dim, head_dim)
self.k = nn.Linear(embed_dim, head_dim)
self.v = nn.Linear(embed_dim, head_dim)
def forward(self, hidden_state):
attention_outputs = scaled_dot_product_attention(
self.q(hidden_state), self.k(hidden_state), self.v(hidden_state))
return attention_outputs
class Multi_Head_Attention(nn.Module):
def __init__(self, config):
super().__init__()
embed_dim = config.hidden_size
head_num = config.num_attention_heads
head_dim = embed_dim // head_num
self.heads = nn.ModuleList(
[Attention_Head(embed_dim, head_dim) for _ in range(head_num)]
)
self.output_linear = nn.Linear(embed_dim, dembed_dim)
def forward(self, hidden_state):
x = torch.cat([h(hidden_state) for h in self.heads], dim=-1)
x = self.output_linear(x)
return x
class FeedForward(nn.Module):
def __init__(self, config):
super().__init__()
self.linear_1 = nn.Linear(config.hidden_size, config.intermediate_size)
self.linear_2 = nn.Linear(config.intermediate_size, config.hidden_size)
self.gelu = nn.GELU()
self.dropout = nn.Dropout(config.hidden_dropout_prob)
def forward(self, x):
x = self.linear_1(x)
x = self.gelu(x)
x = self.linear_2(x)
x = self.dropout(x)
return x
class Transformer_Encoder(nn.Module):
def __init__(self, config):
super().__init__()
self.embeddings = Embeddings(config)
self.layers = nn.ModuleList([TransformerEncoder(config)
for _ in range(config.num_hidden_layers)])
self.layer_norm_1 = nn.LayerNorm(config.hidden_size)
self.layer_norm_2 = nn.LayerNorm(config.hidden_size)
self.attetion = Multi_Head_Attention(config)
self.feed_forward = FeedForward(config)
def forward(self, x):
x = self.embeddings(x)
for layer in self.Layers:
x = Layer(x)
hidden_state = self.layer_norm_1(x)
x = x + self.attention(hidden_state)
x = x + self.feed_forward(self.layer_norm_2(x))
return x