반응형

Transformer는 사실, NLP 분야뿐만 아니라, 다양한 분야에서 많이 사용되기 때문에, 그만큼 구현 소스를 쉽게 찾을 수 있다. 나도, Transformer를 자주 사용하지만, 라이브러리에서 읽어오는 형태로 사용하기 때문에, 그 상세 구조에 대해서는 대략적으로만 알고 있다. 이번 기회에 Transformer를 pytorch로 직접 짜보면서 그 구조를 정확히 이해하고자 한다.

 

Full source : https://github.com/daehwichoi/transformer-pytorch/blob/main/model/transformer.py

 

구현 방향

  • 사실, pytorch로 Transformer를 구현한 사례는 google 검색만 해도 굉장히 많이 나온다. 하지만, original transformer를 직접 구현해보고 싶어서, 논문을 그대로 구현하는데 초점을 맞췄다. 
  • 모델 학습을 위한 layer(Dropout 등)나, dataloader는 task마다 다르고, 구현 목적이 Transformer 모델을 구현하는 것이기 때문에, 모델 구현만 진행했다.

 

참고 자료

  • Transformer 논문 내, 구조 설명 부분

2023.05.08 - [NLP 논문] - Transformer (Attention Is All You Need) - (1) 리뷰

 

Transformer (Attention Is All You Need) - (1) 리뷰

Transformer 배경 설명 Transformer는 Google Brain이 2017년 "Attention is All You Need"라는 논문에서 제안된 딥러닝 모델이다. Transformer는 기존 자연어 처리 분야에서 주로 사용되던 RNN, LSTM 같은 순환 신경망 모

devhwi.tistory.com

 

 

구조 설명

  • Transformer는 크게 Encoder 부분과 Decoder 부분, input&output embedding, postional encoding으로 나뉜다.
  • Encoder 부분은 N개의 Encoder가 연결된 구조로 구성되어 있고, Decoder도 N개의 Decoder가 연결된 구조로 구성되어 있다.
  • Ecoder는 크게, Multi-Head Attention(self-attention)과 redidual 부분(residual add  & layer norm), Feed Forward로 구성되어 있다. 
  • Decoder는 크게, Masked-Multi-Head Attention(self-attention)과 resiedidual 부분(residual add  & layer norm), Multi-Head Attention(encoder-decoder attention), Feed Forward로 구성되어 있다.

 

구현 내용 설명

(순서는 내가 구현한 순서이다.)

1. Multi-Head Attention

[Sacled Dot-Product Attention]

  • Multi-Head Attention의 핵심은 scaled_dot_product_attention이다.
  • scaled_dot_product는 Query, Key, Value가 있을 때, Query와 Key의 Transpose의 Matmul(Dot Product)를 통해, similarity를 계산하고, similarity 기반으로 Value 값을 참조한다.
  • Scaled Dot-product는 Network 여러 부분에서 사용되지만, Decoder 부분에서는 masking 처리를 해야 하는 부분이 있기 때문에, mask부분을 포함해서 함께 구현했다.

    def scaled_dot_product_attention(self, q, k, v, mask=None):
        d_k = k.size()[-1]
        k_transpose = torch.transpose(k, 3, 2)

        output = torch.matmul(q, k_transpose)
        output = output / math.sqrt(d_k)
        if mask is not None:
            output = output.masked_fill(mask.unsqueeze(1).unsqueeze(-1), 0)

        output = F.softmax(output, -1)
        output = torch.matmul(output, v)

        return output

[Multi-Head Attention]

  • Multi-Head Attention은 scaled Dot-Product Attention을 query에 해당하는 value 값들을 참조하기 위해 사용하는데, query, key, value를 그대로 사용하는 것이 아니라, 여러 개의 head로 나누고, query, key, value를 linear projection 한 후, 사용한다. 
  • Scaled Dot-Product Attention 이후, 각 head의 value 값을 concat하고, linear layer을 거쳐 output을 낸다.
  • 주의할 점은, sequence의 순서가 중요하기 때문에, contiguous를 사용해서, 순서를 유지한다는 점이다.

class MultiHeadAttention(nn.Module):
    def __init__(self, dim_num=512, head_num=8):
        super().__init__()
        self.head_num = head_num
        self.dim_num = dim_num

        self.query_embed = nn.Linear(dim_num, dim_num)
        self.key_embed = nn.Linear(dim_num, dim_num)
        self.value_embed = nn.Linear(dim_num, dim_num)
        self.output_embed = nn.Linear(dim_num, dim_num)

    def scaled_dot_product_attention(self, q, k, v, mask=None):
    ...
    
    def forward(self, q, k, v, mask=None):
        batch_size = q.size()[0]

        # 순서 유지 때문에 view 후 transpose 사용
        q = self.query_embed(q).view(batch_size, -1, self.head_num, self.dim_num // self.head_num).transpose(1, 2)
        k = self.key_embed(k).view(batch_size, -1, self.head_num, self.dim_num // self.head_num).transpose(1, 2)
        v = self.value_embed(v).view(batch_size, -1, self.head_num, self.dim_num // self.head_num).transpose(1, 2)

        output = self.scaled_dot_product_attention(q, k, v, mask)
        batch_num, head_num, seq_num, hidden_num = output.size()
        output = torch.transpose(output, 1, 2).contiguous().view((batch_size, -1, hidden_num * self.head_num))

        return output

 

2. Residual Add & Layer Norm

[Layer Norm]

  • Layer Norm은 dimension layer 방향으로 평균을 빼고, 표준 편차로 나누는 Normalization 기법이다.
  • 이 부분은 nn.LayerNorm을 통해, 구현할 수 있다. 
   def layer_norm(self, input):
        mean = torch.mean(input, dim=-1, keepdim=True)
        std = torch.std(input, dim=-1, keepdim=True)
        output = (input - mean) / std
        return output

[Add & Layer Norm]

  • 이전 층의 output을 layer norm을 통해, normalization 한 후, residual 값을 더해준다.
class AddLayerNorm(nn.Module):
    def __init__(self):
        super().__init__()

    def layer_norm(self, input):
    ...

    def forward(self, input, residual):
        return residual + self.layer_norm(input)

 

3. Feed Forward

  • Feed Forward는 Fully Connected Layer → Relu →  Fully Connected Layer로 구성되어 있다.

class FeedForward(nn.Module):
    def __init__(self, dim_num=512):
        super().__init__()
        self.layer1 = nn.Linear(dim_num, dim_num * 4)
        self.layer2 = nn.Linear(dim_num * 4, dim_num)

    def forward(self, input):
        output = self.layer1(input)
        output = self.layer2(F.relu(output))

        return output

 

4. Encoder

  • Encoder는 Multi-Head Attention → Residual Add & Layer Norm → Feed Forward → Residual Add & Layer Norm 순으로 구성되어 있다. 
  • Encoder는 단순히, 앞서 선언했던, sub layer들을 연결하는 방식으로 구현했다.

class Encoder(nn.Module):
    def __init__(self, dim_num=512):
        super().__init__()
        self.multihead = MultiHeadAttention(dim_num=dim_num)
        self.residual_layer1 = AddLayerNorm()
        self.feed_forward = FeedForward(dim_num=dim_num)
        self.residual_layer2 = AddLayerNorm()

    def forward(self, q, k, v):
        multihead_output = self.multihead(q, k, v)
        residual1_output = self.residual_layer1(multihead_output, q)
        feedforward_output = self.feed_forward(residual1_output)
        output = self.residual_layer2(feedforward_output, residual1_output)

        return output

 

5. Decoder

  • Decoder는 Masked Multi-Head Attention → Residual Add & Layer Norm → Multi-Head Attention → Residual Add & Layer Norm Feed Forward → Residual Add & Layer Norm 순으로 구성되어 있다.
  • Encoder와 마찬가지로, 앞서 구현해놓은 sub-layer를 연결하면 되지만, 중간 Multi-Head Attention은 Query와 Key를 Encoder의 Output을 사용하기 때문에, 이 점을 명시해야 한다.
  • Decoder는 Ecoder와 다르게, masking을 이용하여, mask를 인자로 받는 것도 주의해야 한다.

class Decoder(nn.Module):
    def __init__(self, dim_num=512):
        super().__init__()

        self.masked_multihead = MultiHeadAttention(dim_num=dim_num)
        self.residual_layer1 = AddLayerNorm()
        self.multihead = MultiHeadAttention(dim_num=dim_num)
        self.residual_layer2 = AddLayerNorm()
        self.feed_forward = FeedForward(dim_num=dim_num)
        self.residual_layer3 = AddLayerNorm()

    def forward(self, o_q, o_k, o_v, encoder_output, mask):
        masked_multihead_output = self.masked_multihead(o_q, o_k, o_v, mask)
        residual1_output = self.residual_layer1(masked_multihead_output, o_q)
        multihead_output = self.multihead(encoder_output, encoder_output, residual1_output, mask)
        residual2_output = self.residual_layer2(multihead_output, residual1_output)
        feedforward_output = self.feed_forward(residual2_output)
        output = self.residual_layer3(feedforward_output, residual2_output)

        return output

 

6. Transformer

  • 전체 Transformer는 Input Embedding, Positional Encoding, Output Embedding, N개의 encoder와 N개의 decoder로 구성되어 있다. 

[positional_encoding]

  • positinal encoding은 짝수번째 token과 홀수번째 token이 각기 다른 식을 따른다. 아래 식에서 i는 hidden dimension 방향의 index이고, pos는 positional 방향(몇 번째 seq인지)을 의미한다.
  • positional encoding은 크게, 두 부분에서 사용되는데, Input과 Output의 sequence length 길이가 다를 수 있기 때문에, 이것을 인자로 받는 형태로 구현했다.
  • 마지막에 self.register_buffer는 추후, model parameter 학습 시, psotional encoding이 학습되지 않도록 막아주기 위한 용도이다. 

    def position_encoding(self, position_max_length=100):
        position = torch.arange(0, position_max_length, dtype=torch.float).unsqueeze(1)
        pe = torch.zeros(position_max_length, self.hidden_dim)
        div_term = torch.pow(torch.ones(self.hidden_dim // 2).fill_(10000),
                             torch.arange(0, self.hidden_dim, 2) / torch.tensor(self.hidden_dim, dtype=torch.float32))
        pe[:, 0::2] = torch.sin(position / div_term)
        pe[:, 1::2] = torch.cos(position / div_term)
        pe = pe.unsqueeze(0)
        self.register_buffer('pe', pe)

        return pe

 

[input & output Embedding]

  • Embedding은 nn.Embedding을 통해, 쉽게 구현할 수 있다.
  • Embedding의 첫 번째 인자는 input 데이터의 total word 개수, 두 번째 인자는 hidden dimension의 수이다.
  • total_word_num은 sequence dictionary에 존재하는 unique value의 개수를 의미한다. (전체 단어가 아님을 주의)
  • 사실 편의를 위해, 공통 total_word_num을 사용했는데, 번역과 같은 경우, input의 단어 개수와 output의 단어 개수가 다를 수 있어, task에 따라서는 다른 인자를 받는 게 맞다.
 self.input_data_embed = nn.Embedding(total_word_num, self.hidden_dim)
 self.output_data_embed = nn.Embedding(total_word_num, self.hidden_dim)

 

[Transformer]

  • Transformer의 Encoder 부분은 앞서 구현했던, Encoder를 N번 반복하는 구조로 구현되어 있다. 
  • Encoder 부분에 들어가는 query, key, value는 문장의 embedding 한 값으로 모두 같고, (참조를 위한 query와 key가 비효율적이다.) 전번째 encoder의 결과가 다음 encoder의 query, key, value가 된다.
  • Decoder 부분도 비슷하지만, Encoder의 output이 사용된다는 점, Decoder 단에서는 다음 sequence를 볼 수 없기 때문에, 그 부분을 처리하기 위한 mask가 존재한다는 점이 다르다. 
  • Decoder에 masking으로 0 값을 넣어주었지만, 실제 학습해서는 매우 작은 값을 넣어주는 것이 학습 측면에서 유리하다고 한다.
  • Encoder 부분과 Decoder 부분을 모두 거치면, 목적에 맞는 fully connected layer를 연결하여, output을 낸다. 
class Transformer(nn.Module):
    def __init__(self, encoder_num=6, decoder_num=6, hidden_dim=512, max_encoder_seq_length=100,
                 max_decoder_seq_length=100):
        super().__init__()

        self.encoder_num = encoder_num
        self.hidden_dim = hidden_dim
        self.max_encoder_seq_length = max_encoder_seq_length
        self.max_decoder_seq_length = max_decoder_seq_length

        self.input_data_embed = nn.Embedding(max_seq_length, self.hidden_dim)
        self.Encoders = [Encoder(dim_num=hidden_dim) for _ in range(encoder_num)]

        self.output_data_embed = nn.Embedding(max_seq_length, self.hidden_dim)
        self.Decoders = [Decoder(dim_num=hidden_dim) for _ in range(decoder_num)]

        self.last_linear_layer = nn.Linear(self.hidden_dim, max_seq_length)

    def position_encoding(self, position_max_length=100):
    ...

    def forward(self, input, output, mask):

        input_embed = self.input_data_embed(input)
        input_embed += self.position_encoding(self.max_encoder_seq_length)
        q, k, v = input_embed, input_embed, input_embed

        for encoder in self.Encoders:
            encoder_output = encoder(q, k, v)
            q = encoder_output
            k = encoder_output
            v = encoder_output

        output_embed = self.output_data_embed(output)
        output += self.position_encoding(self.max_decoder_seq_length)
        output_embed = output_embed.masked_fill(mask.unsqueeze(-1), 0)
        d_q, d_k, d_v = output_embed, output_embed, output_embed

        for decoder in self.Decoders:
            decoder_output = decoder(d_q, d_k, d_v, encoder_output, mask)
            d_q = decoder_output
            d_k = decoder_output
            d_v = decoder_output

        output = F.softmax(self.last_linear_layer(decoder_output), dim=-1)
        return output

 

총평

  • 실제 NLP 단어 예측 등, 데이터셋을 넣어보기 위해, dataloader와 학습 등을 연결해 봐야겠다.
  • 특정 task를 풀기 위해, 데이터셋을 처리하기 위한 model을 짜는 것도 좋지만, 가끔은 논문을 그대로 구현해 보는 것도 좋을 것 같다. 특히, 그림과 글만 보고 구현을 하려고 하니, 내가 정확하게 알지 못했던 부분, 특히 머리로 이해하고 넘어간 부분을 완전히 알게 된 것 같아 좋다. 
반응형

Introduction

  • Pytorch 학습 중, Resource와 모델 구조에 대한 profiling은 torch profiler를 이용해 가능하였다.

2023.07.09 - [Python] - Pytorch 구조 & Resource Profiler 도구 (torch profiler)

 

Pytorch Resource & 모델 구조 Profiler 도구 (torch profiler)

Introduction 딥러닝 학습을 잘(?)한다는 것을 정의하기는 어렵지만, 더 빠른 시간 안에 많은 양을 학습하는 것은 매우 중요하다. 딥러닝의 모델은 다수의 layer로 구성되어 있기 때문에, 각 layer의 결

devhwi.tistory.com

  • 이때, profiling의 결과는 테이블 구조의 텍스트 형태로 터미널에 출력 or 파일에 저장 가능하다.
  • 이 결과로도 Insight를 충분히 추출할 수 있지만, 텍스트 형태로 분석하다 보면, 가시성이 떨어진다는 점과, profiling 이력 간 비교가 어렵다는 점이 아쉽다.
  • 이를 해결할 수 있는 딥러닝의 시각화 툴인 Tensorboard에 profiling 결과를 올리는 방법이 있어서, 이 방법을 알아보고자 한다.

 

원리

  • torch profiler의 옵션에 "on_trace_ready"라는 옵션이 존재한다. 이것은 profiling 결과가 준비되었을 때, 호출할 callback 함수를 지정하는 것인데, 이 callback 함수로 tensorboard에서 제공하는 trace handler를 연결하여, tensorboard에서 읽을 수 있는 log 형태로 떨궈준다.

 

Setup

  • tensorboard에서 torch profiler의 결과를 읽어서 표현할 수 있는 plugin을 추가로 설치해야한다. (당연히, tensorboard가 필요하기 때문에, 아래 plugin을 설치하면, tensorboard도 자동으로 설치된다.)
pip install torch_tb_profiler

 

사용법

  • torch profiler를 이용하기 위한, 콘텍스트 관리자(with 절)에  on_trace_ready 옵션에, "tensorboard_trace_handler" 함수를 지정해 준다.
  • 이때, tensorboard_trace_handler 함수의 인자로, tensorboard에서 읽을 수 있도록 log 디렉터리를 지정해 준다.
  ...
  with profile(activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA], record_shapes=True,
                 profile_memory=True, on_trace_ready=torch.profiler.tensorboard_trace_handler('./log/resnet18')) as prof:
        for epoch in range(TRAIN_EPOCH):
            running_loss = 0.0
            for i, data in enumerate(trainloader, 0):
                inputs, labels = data[0].to(device), data[1].to(device)
                optimizer.zero_grad()
                with record_function("model_inference"):
                    outputs = model(inputs)
                loss = criterion(outputs, labels)
                loss.backward()
                optimizer.step()

                running_loss += loss.item()
                if i % TRAIN_PRINT_FREQUENCY == TRAIN_PRINT_FREQUENCY - 1:
                    print(f'Epoch: {epoch + 1}, Batch: {i + 1}, Loss: {running_loss / 200:.3f}')
                    running_loss = 0.0
  ...
  • 그 후에, tensorboard 실행을 해주면 끝난다.
tensorboard --logdir=./log/resnet18

 

결과

[NORMAL]

  • Overview : Overview는 전체적인 프로파일링의 결과에 대해 보여주는 화면이다.
    • Configuration : Profiling 시 사용된 설정 정보를 표시한다. 
    • Execution Summary : 전체 수행 시간과, 각 단계에 소요된 수행시간을 보여준다.
    • Spen Time Breakdown : 코드 또는 연산의 실행 시간을 단계별(Kernel, Memcpy, Memset, Runtime, DataLoader, CPU Exec, Other)로 분해하여 보여준다. 
    • Performance Recommentation : profiling 결과를 기반으로 한 성능 개선 권장 사항을 자동으로 생성해 준다. (실제로 어떤 원리로 동작하는지는 잘 모른다.)

  • Operator : 연산에 대한 profiling 결과를 보여준다. torch profiling 결과를 터미널에서 출력하였을 때, 보여주던 결과를 그대로 보여준다고 생각하면 된다. 추가적으로 Tensor Cores Eligible 옵션이 있는데, 해당 연산이 GPU를 사용할 수 있는지에 대한 가능 여부를 표시한 것이다. Group By 조건을 바꾸면, input shape도 볼 수 있다.

 

  • Trace : 함수 및 연산의 실행 시간을 시간 경과에 따라 그래프 형태로 표시한다. 해당 코드의 실행에 사용된 Process와 그 안의 Thread의 동작을 확인 할 수 있다.  (사실 이 UI는 torch profiler의 chrome tracing 기능으로도 볼 수 있다.)

 

  • Memory : 실행 시간에 따른 Memory 사용 추이를 보여준다. 각 연산마다 할당한 메모리와, Allocation Time과 Release Time, Duration을 보여준다. 코드 실행에 사용한 H/W 별로 볼 수 있다. (다만, 해당 UI에서 Memory를 많이 사용하는지, Chrome이 계속 죽는다.)

 

 

[DIFF]

  • Tensorboard를 통한 torch profiler 시각화의 가장 큰 장점이라고 할 수 있는 이력 간 비교 기능이다. Baseline의 log를 정한 뒤, 비교하고자 하는 log를 대입하면, 그 둘 간의 profiling 결과의 delta 값을 보여준다. 
  • 이를 통해, Profiling 결과를 비교하면서, H/W 효율화를 위한 구조 개선을 진행할 수 있다.

 

 

Torch 모델에서 torch profiling을 시각화하여 비교하는 방법을 알아보았다. 개인 프로젝트에서는 그 효용이 덜하겠지만, 많은 사람들이 같은 모델을 연구할 때, 성능과 profiling 결과를 모두 tensorboard를 통해 시각화하여, 성능을 유지하면서 모델의 연산 효율성을 향상하거나, 모델의 연산  효율성을 유지하면서 모델의 성능을 향상하는 데, 사용하면 매우 유용할 것이다. 

반응형

Introduction

  • 딥러닝 학습을 잘(?)한다는 것을 정의하기는 어렵지만, 더 빠른 시간 안에 많은 양을 학습하는 것은 매우 중요하다.
  • 딥러닝의 모델은 다수의 layer로 구성되어 있기 때문에, 각 layer의 결과가 데이터가 어느 형태로 존재하는지, 어느 layer가 병목 현상인지를 파악하는 것이 까다롭다.
  • 특히, 최근에는 모델을 직접 코드로 구현하기보다는,  pre-trained model을 사용하는 경우가 많은데, 사전에 사용하는 모델의 구조를 알지 못하면, 내부 동작을 제대로 파악할 수 없다.
  • Pytorch에서는 이러한 모델의 구조와 각 layer에서의 cost를 profiling 할 수 있는 torch profiler를 지원한다.

 

Code Sample

  • torch profiler 테스트를 위한 resnet18을 이용한 CIFAR-10 classification code이다. 
  • 모델을 직접 코드로 구현한 것이 아닌, torchvision에서 load 하였다.
  • 사용 상황을 가정하자면, load 한 모델의 구조를 모르거나, 모델에 부하가 존재하는 부분을 tuning 해야 하는데, 어느 layer를 바꿔야 할지 모르는 상황이다. 
import torch
import torchvision
import torchvision.transforms as transforms
import torch.nn as nn
import torch.optim as optim

TRAIN_EPOCH = 10
TRAIN_PRINT_FREQUENCY = 200

if __name__ == '__main__':
    transform = transforms.Compose([
        transforms.RandomHorizontalFlip(),
        transforms.RandomCrop(32, padding=4),
        transforms.ToTensor(),
        transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
    ])

    trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
    trainloader = torch.utils.data.DataLoader(trainset, batch_size=128, shuffle=True, num_workers=2)

    testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
    testloader = torch.utils.data.DataLoader(testset, batch_size=100, shuffle=False, num_workers=2)

    model = torchvision.models.resnet18(pretrained=False)
    num_features = model.fc.in_features
    model.fc = nn.Linear(num_features, 10)

    criterion = nn.CrossEntropyLoss()
    optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model = model.to(device)

    for epoch in range(TRAIN_EPOCH):
        running_loss = 0.0
        for i, data in enumerate(trainloader, 0):
            inputs, labels = data[0].to(device), data[1].to(device)
            optimizer.zero_grad()
            outputs = model(inputs)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()

            running_loss += loss.item()
            if i % TRAIN_PRINT_FREQUENCY == TRAIN_PRINT_FREQUENCY - 1:
                print(f'Epoch: {epoch + 1}, Batch: {i + 1}, Loss: {running_loss / 200:.3f}')
                running_loss = 0.0
    print("Training finished.")

    correct = 0
    total = 0
    with torch.no_grad():
        for data in testloader:
            images, labels = data[0].to(device), data[1].to(device)
            outputs = model(images)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()

    accuracy = correct / total
    print(f'Test Accuracy: {accuracy:.2%}')

 

Torch Profiler

[Setup]

  • Pytorch에서는 1.8 버전 이상부터 torch의 profiling을 위한 torch.profiler를 제공한다. 따라서, Torch 버전이 1.8 이상인 경우에는 별도의 설치가 필요 없다. 
  • 현재 설치된 Torch 버전을 잘 모른다면, Python에서 아래 명령어를 통해 확인해 보자.
import torch
print(torch.__version__)
  • 만약, torch 버전이 1.8 미만에 torch 버전을 바꿔도 문제가 없는 상황이라면, 아래 명령어를 통해 torch 버전을 업그레이드해 준다. 
pip install --upgrade torch torchvision

 

[사용법]

  • 사용방법은 매우 간단하다. 우선 torch.profiler를 import 하고, 콘텍스트 관리자(with 절)를 이용하여, profiling을 위한 부분을 감싸주면 된다. (함수 전체에 대한 profiling은 profile with 절을 @profile(acitivities~)와 같은 decorator로 처리할 수 있다.)
  • 아래는 sample code 중, train에 대한 profiling을 위한 소스이다. 주의할 점은, profiling에 memory가 많이 소모되기 때문에, train epoch을 1로 낮춰놓고 profiling을 하는 것이 좋다. (어차피, 같은 동작이 반복되기 때문에, input 하나만을 측정해도 별 문제는 없다.)
  • 모델을 GPU에서 돌려서  "ProfilerActivity.CUDA "를 포함했지만, CPU로 돌리는 환경에서는 해당 인자를 생략해도 된다. (다만, GPU 환경에서는 CPU, CUDA 인자 모두 필요함)
...
from torch.profiler import profile, record_function, ProfilerActivity
...

    with profile(activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA], record_shapes=True) as prof:
        for epoch in range(TRAIN_EPOCH):
            running_loss = 0.0
            for i, data in enumerate(trainloader, 0):
                inputs, labels = data[0].to(device), data[1].to(device)
                optimizer.zero_grad()
                with record_function("model_train"):
                    outputs = model(inputs)
                loss = criterion(outputs, labels)
                loss.backward()
                optimizer.step()

                running_loss += loss.item()
                if i % TRAIN_PRINT_FREQUENCY == TRAIN_PRINT_FREQUENCY - 1:
                    print(f'Epoch: {epoch + 1}, Batch: {i + 1}, Loss: {running_loss / 200:.3f}')
                    running_loss = 0.0
    print(prof.key_averages().table(sort_by="self_cpu_time_total"))
    print("Training finished.")

[인자 설명]

  • activities : list 형태로 입력받는다. 어떤 활동을 profiling 할 것인지를 지정한다. 가능한 활동은 다음과 같다.
    • ProfilerActivity.CPU : CPU 작업(연산, 함수 호출)에 대한 프로파일링, CPU 시간, 메모리 사용량등을 제공
    • ProfilerActivity.CUDA  : CUDA 작업(GPU 연산, 호출)에 대한 프로파일링, GPU 시간, 메모리 사용량등을 제공
  • record_shapes : bool 형태, 각 layer의 입력(input)을 기록할지 여부
  • profile_memory : bool 형태, memory를 profiling 할지 여부, False로 설정하면 time에 대한 profiling만 진행한다.
  • on_trace_ready : Profiling 결과가 준비되었을 때, 호출될 callback 함수를 지정할 수 있음. on_trace_ready 옵션을 통해, 함수를 사전 정의해, profiling 결과 등을 file 형태로 떨굴 수 있다.
  ...
    with profile(activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA], record_shapes=True,
                 profile_memory=True, on_trace_ready=finish_profiler) as prof:
  ...
  • with_stack : bool 형태, 함수 호출 stack 정보를 표기할지에 대한 여
  • with_flops : bool 형태, 실제로 계산 비용을 FLOPs로 측정한 결과 
  • with_modules : bool 형태, profiling 결과에 연산의 호출 stack에 대한 module의 계층 구조를 기록해 줌. (어떤 연산이 어떤 연산의 내부에서 호출되었는지를 나타내줌)

 

 

[결과 출력]

  • 결과는 다음과 같은 명령어로 호출할 수 있다.
print(prof.key_averages().table(sort_by="self_cpu_time_total"))
  • 결과는 table 형태로 보이는데, 아래와 같이 다양한 옵션들을 사용할 수 있다. (key_averages() 내의 인자 형태로 들어감)
    • group_by_input_shape : True로 설정하면, 동일한 입력 모양을 가진 연산 또는, 함수 호출을 grouping 할 수 있다. (모델의 input 사이즈를 보려면, 해당 옵션을 True 설정해야 한다.)
    • group_by_stack_n : 연산 또는 함수의 stack의 상위 n 단계만을 기준으로 grouping 할지 지정하는 인자
  • table의 인자도 지정할 수 있는데, table의 출력에 대한 옵션을 지정한다.
    • sort_by : table을 어떤 기준으로 order by 할지 (default : None)
    • row_limit : 몇 개까지 표시할지
    • header : header를 표시할지 (default : None)
    • top_level_events_only : 해당 옵션을 True 설정하면, 최상위 호출 단계까지만 표시
  • 아래는 sample code에 대해 profiling을 수행한 결과이다. (CPU 시간이 큰 10개만 추출)

  • 결과에서 보이는 각 칼럼은 다음과 같다.  
  • CPU Time 관련 
    • Self CPU % : 연산 or 함수 호출이 소비한 CPU 시간의 백분율 (전체 실행 시간에서 해당 연산이 소요한 CPU 시간)
    • Self CPU : 해당 연산 or 함수 호출이 소비한 총 CPU 시간
    • CPU  total % : 해당 연산 or  함수 호출과 그 하위 호출에서 소요된 총 CPU 시간의 백분율
    • CPU total : 해당 연산 or 함수 호출과 그 하위 호출에 의해 사용된 총 CPU 시간
    • CPU time avg : 해당 연산 or 함수 호출의 평균 CPU 시간 (평균적으로 해당 연산이 소요되는 시간)
  • CUDA Time 관련  
    • Self CUDA : 해당 연산 or 함수 호출이 소비한 총 CUDA 시간
    • Self CUDA % : 해당 연산 or 함수 호출이 소비한 총 CUDA 시간의 백분율
    • CUDA total : 해당 연산 or 함수호출과 그 하위 호출에서 소요된 총 CUDA 시간
    • CUDA time avg : 해당 연산 or 함수호출과 그 하위 호출에서 소요된 평균 CUDA 시간
    • # of Calls : 해당 연산 또는 함수 호출의 호출 횟수
  • Model Input 관련 
    • Input Shapes : record shapes를 True로 하고, key_averages에 group_by_input_shape를 true로 지정한 경우에만 보인다. 각 연산의 input shape이 보인다.
  • CPU memory 관련 (snapshot 형태기 때문에 사용 전과 후의 memory 사용 delta값이 나온다. 즉, 음수가 될 수 있다.) 
    • CPU Mem : 연산 or 함수 호출이 소비한 CPU의 메모리 총 용량 
    • Self CPU Mem : 연산 or 함수 호출이 직접적으로 사용한 CPU 메모리 용량
  • CUDA memory 관련 (snapshot 형태기 때문에 사용전과 후의 memory 사용 delta값이 나온다. 즉, 음수가 될 수 있다.)
    • CUDAMem : 연산 or 함수 호출이 소비한 CUDA의 메모리 총 용량 
    • Self CUDAMem : 연산 or 함수 호출이 직접적으로 사용한 CPU 메모리 용량
  • 연산량 관련
    • Total MFLOPs : 연산 or 함수 호출이 실행될 때, 총 수행된 MFLOPs 수 

 

Torch 모델에서 torch Profiling을 통해, 부하가 되는 부분이나, Layer의 input size 등을 확인할 수 있다. 해당 profiling은 모델에서 부하가 되는 부분을 개선하거나, 하드웨어 확장에 대한 의사결정, Batch size 조절 등 다양한 model 개선에 사용될 수 있다.

반응형

회사에서는 ML Ops 환경이 어느 정도 갖춰져 있어서, Kubeflow를 직접 설치하지 않았다. Local에서 Kubeflow를 설치하려고 하니, 생각보다 쉽지 않았다.  시간은 조금 걸렸지만, 설치하는 과정을 적어보고자 한다.


WSL 설치

  • Window에서 Kubernetes를 설치하기 위해서는, WSL(Windows Subsystem for Linux) 환경을 구성해야한다.
  • 먼저, Windows 검색 창에 Windows 기능 켜기/끄기를 검색하여 실행한다.
  • Windows 기능 켜기/끄기 List들에서 Linux용 Windows 하위 시스템을 체크해 준다. Windows Powershell을 관리자 권한으로 실행한 뒤,  wsl이 설치되어 있는지 확인한다. (보통 설치되어 있을 가능성이 높아서 확인)

wsl -l -v
  • 아래와 같이, list에 WSL 항목이 뜨고, VERSION이 '2' 이면, Docker Desktop 설치로 넘어간다. 

 

  • 만약 버전이 '1'일시에는, 아래 명령어로 버전을 바꿔준다.  Powershell에서 아래 명령어를 입력해 준다.
wsl --set-version {Name} 2  # Name에 현재 사용중인, wsl 버전 입력
wsl -l -v  # 적용 확인
wsl # wsl 환경으로 진입
  • 만약 wsl 목록에 존재하지 않는다면, Microsoft Store에서 "Ubuntu" 검색 후, 원하는 버전을 설치해 준다.

 

Docker Desktop 설치

  • Kubeflow는 Kubernetes 환경 하에서 돌아간다. 따라서, Kubeflow 설치 전에 Kubernetes 환경을 먼저 구성해야 한다.
  • Window에서 가장 쉽게 Kubernetes를 설치하는 방법은 Docker Desktop을 통해, 설치하는 것이다. 
  • 우선, 아래 Link에서 Docker Desktop을 설치한다.
Docker Desktop 설치
https://www.docker.com/products/docker-desktop/

Docker Desktop 설치 화면 (출처: https://www.docker.com/products/docker-desktop/)

  • 설치 후, Docker Desktop을 켜고, Setting > Kubernetes > Enable Kubernetes > Apply & restart를 눌러준다.

 

Kubectl 설치

  • Kubectl은 Kubernetes 클러스터를 관리하기 위한 CLI 도구로, 아래의 명령어를 통해 설치한다. 
sudo apt-get update && sudo apt-get install -y kubectl
kubectl version

 

Minikube 설치 & 실행

  • H/W 자원이 만약 풍부하다면, 일반의 Kubernetes Cluster 구성(마스터와 노드를 포함)이 가능하겠지만, 로컬 환경에서는 대체로 H/W 자원이 그리 풍부하지 않다.
  • Minikube는 로컬에서도 개발 & 테스트를 용이하게 하기 위해, 단일 노드로 구성된 Kubernetes Cluster를 생성할 수 있도록 하는 도구이다. 
  • 아래 명령어로, minikube를 설치한다.
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube_latest_amd64.deb && sudo dpkg -i minikube_latest_amd64.deb
  • minikube를 설치했으면, 아래 명령어를 통해, Kubernetes Cluster를 실행해 준다. (memory와 cpu, disk 등은 현재 사용 가능한 리소스 내에서 적당히 설정해 준다. 너무 많이 설정하면, Local 환경이 제대로 동작하지 않는다.)
  • 현재(23/03) 기준으로 Kubeflow가 지원하는 가장 높은 수준의 Kubernetes 버전은 1.25.0이다. 
minikube start --driver=docker --memory=4g --cpus=2 --disk-size 20GB --kubernetes-version=1.25.0

※ 주의 : kubernetes 버전에 따라, 사용 가능한, kubeflow 버전이 달라진다. 

 

Kustomize 설치 (Optional)

  • Kustomize는 원본 yaml 파일의 변경 없이, yaml 파일을 변경할 수 있도록 하여, 환경에 맞는 설치를 가능하도록 도와주는 도구이다.
  • 사실, Kubectl 설치 시, Kustomize는 자동으로 설치된다. (Kustomize 버전을 업그레이드하기 위해서는, Kubectl 버전을 업그레이드해야 한다.) 
  • 다만, Kustomize와 minikube의 Kubernetes 버전이 호환이 안 되는 경우, Kustomize만 따로, 설치하여, 적용 가능하다. 
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
sudo cp kustomize /usr/local/bin/
kustomize version

 

Kubeflow 설치

  • 드디어, 대망의 Kubeflow를 설치한다.
  • Kubeflow 설치는 Kustomize를 이용하여, Kubernetes 버전에 존재하는 각 Component 들의 yaml 파일을 변경하여, 설치하는 구조이기 때문에, 1) Kubernetes 버전 2) Kustomize 버전과 호환성을 맞춰줘야 한다. 
  • 계속, Kubeflow 버전이 나오고 있기 때문에, 명령어 입력 전, https://github.com/kubeflow/manifests#installation 에 접속하여, 버전을 확인해 보기 바란다.

23/03/27 기준 prerequisites

  • 내가 설정한 버전은 다음과 같다.
kubernetes = 1.25
kustomize = 5.0.0
kubeflow = v1.7

 

git clone --branch v1.7-branch https://github.com/kubeflow/manifests.git
cd manifests
# 버전 변경이 필요 시 : git checkout (branch 명)
while ! kustomize build example | awk '!/well-defined/' | kubectl apply -f -; do echo "Retrying to apply resources"; sleep 10; done

※ 설정한 H/W 리소스에 따라 다르겠지만, 설치까지 정말 오래 걸린다. 터미널 문구에 Warning이 뜨는 것은 상관없는데, Error가 뜬다면, Kubernetes 버전이나, Kustomize 버전과 호환이 안되었을 확률이 높다. 그런 경우 Kubernetes 버전과 Kustomize 버전을 바꿔주던, Kubeflow 버전을 바꿔주던 해야 한다.

 

  • 아무래도, Kubeflow가 특정 Kubernetes 버전에 맞춰서 개발되었을 것이므로, Kubernetes 버전을 바꿔주는 것이 좋다고 느껴진다. 
  • Kubernetes 버전은 아래 명령어로 다시 설정해 준다.
minikube delete
minikube start --driver=docker --memory=4g --cpus=2 --disk-size 20GB --kubernetes-version={새로운 버전}

 

Kubeflow 접속

  • 로컬 컴퓨터의 8080 포트와 쿠버네티스 내부의 80 포트를 연결해 줘서, UI에 접근할 수 있도록 해준다. 
  • 이 명령어를 실행하면 로컬 컴퓨터의 8080 포트를 통해 쿠버네티스 내부의 istio-ingressgateway 서비스에 접근할 수 있다.  
kubectl port-forward svc/istio-ingressgateway -n istio-system 8080:80
  • 명령어 실행 후, 브라우저에서 http://127.0.0.1:8080에 접근하면 Kubeflow UI에 접근 가능하다. 

 


Kubeflow 설치는 시간도 오래 걸리고, 각 도구들의 버전 호환성 때문에, 설치가 어려웠다. 하지만, 천천히 하나하나 진행하다 보면, 대략적으로 Kubeflow가 어떻게 구성되는지 보이는 것 같다. 다음 장부터 Kubeflow 각 component를 파봐야겠다.  

'MLops' 카테고리의 다른 글

MLops 등장: Hidden Technical Debt in Machine Learning Systems 논문 리뷰  (17) 2024.02.18
Kubeflow (2) - Katlib  (1) 2023.04.11
Kubeflow (0) - 소개  (1) 2023.03.26

+ Recent posts