YOLOv4 → Vitis AI DPU 호환 TVM Transform Pass 구현

4 minute read

Published:

개요

Vitis-AI의 Model Zoo1 는 DPU 제약2 을 고려해 이미 최적화된 AI 모델을 제공합니다.

Vitis AI는 Xilinx의 FPGA 플랫폼에서 훈련된 모델을 양자화(Quantization), 컴파일(Compilation), 추론(Inference)하기 위한 통합 플랫폼입니다.

Vitis AI는 TensorFlow, PyTorch, Caffe 등 다양한 프레임워크를 지원하며, TVM도 Vitis-AI의 third_party 디렉토리에 포함되어 있습니다3. 따라서 TVM을 활용하여, Model Zoo에 하드웨어에 최적화된 AI 모델이 올라오기 전에, 직접 하드웨어에 최적화된 AI 모델로 변환할 수 있는 시스템을 조성할 수 있습니다.

본 문서는 TVM의 패턴 변환 패스(Transform Pass)를 활용해 원본 YOLOv4를 DPU 호환 YOLOv4-Leaky로 자동 변환하여, Model Zoo에서 제공하는 결과와 동등한 구조를 자동화하는 방법을 정리합니다.

YOLOv4 변환 필요 사항

YOLOv4를 DPU에서 실행하기 위해서는 두 가지 주요 수정이 필요합니다. 4

1. MISH 활성화 함수 → LeakyReLU 5

MISH 활성화 함수는 다음과 같이 정의됩니다:

\[mish(x) = x \cdot \tanh(\ln(1 + \exp(x)))\]

MISH는 DPU에서 하드웨어 가속을 지원하지 않으므로, LeakyReLU로 대체해야 합니다.

\[LeakyReLU(x) = \begin{cases} ax & \text{if } x < 0 \\ x & \text{if } x \geq 0 \end{cases}\]

여기서 a는 학습 가능한 파라미터이며, 일반적으로 0.1 또는 -0.1을 사용합니다.

2. MaxPool 커널 크기 제한

YOLOv4의 SPP(Spatial Pyramid Pooling) 레이어에서 큰 MaxPool 커널(예: 9x9)을 사용하는데, DPU는 이를 직접 지원하지 않습니다. DPU 제약에 따르면, MaxPool 커널 크기를 8x8 이하로 제한해야 합니다2.

큰 커널을 사용해야 하는 경우, 동일한 receptive field를 유지하면서 여러 개의 작은 커널로 분해할 수 있습니다. 예를 들어, 9x9 MaxPool은 5x5 MaxPool 2회로 분해할 수 있습니다.

TVM Pattern Matching으로 MISH 및 MaxPool 변환 구현

1. MISH 패턴 정의 및 변환

MISH 활성화 함수 패턴(exp → add → log → tanh → multiply)을 정의하고 LeakyReLU로 변환합니다.

from tvm.relax.dpl import pattern  

def create_mish_pattern():
    """MISH 활성화 함수 패턴: x * tanh(log(exp(x) + 1))"""
    input_pat = pattern.wildcard()
    
    # exp(x)
    exp_pat = pattern.is_op("relax.exp")(input_pat)
    
    # exp(x) + 1
    add_pat = pattern.is_op("relax.add")(exp_pat, pattern.is_const(1.0))
    
    # log(exp(x) + 1)
    log_pat = pattern.is_op("relax.log")(add_pat)
    
    # tanh(log(exp(x) + 1))
    tanh_pat = pattern.is_op("relax.tanh")(log_pat)
    
    # x * tanh(log(exp(x) + 1))
    mish_pat = pattern.is_op("relax.multiply")(input_pat, tanh_pat)
    
    return mish_pat, {"input": input_pat}

def create_mish_rewriter():
    """MISH를 LeakyReLU(alpha=0.1)로 변환하는 rewriter"""
    def rewriter(expr, matches):
        input_expr = matches["input"]
        # LeakyReLU(alpha=0.1)로 변환
        return relax.op.nn.leaky_relu(input_expr, alpha=0.1)
    return rewriter

이 패턴은 연산 그래프에서 MISH 활성화 함수의 특정 구조를 감지합니다. exp → add → log → tanh → multiply 순서로 연결된 연산을 찾아서 단일 leaky_relu 연산으로 대체합니다.

2. MaxPool 분해 패턴

9x9 MaxPool을 5x5 MaxPool 두 개로 분해하는 패턴을 정의합니다.

def create_large_maxpool_pattern():
    """9x9 MaxPool 패턴 정의"""
    input_pat = pattern.wildcard()
    
    # 9x9 MaxPool with padding=[4,4,4,4]
    maxpool_pat = pattern.is_op("relax.nn.max_pool2d")(input_pat).has_attr({
        "pool_size": [9, 9],
        "padding": [4, 4, 4, 4]
    })
    
    return maxpool_pat, {"input": input_pat}

def create_maxpool_decomposer():
    """9x9 MaxPool을 5x5 MaxPool 두 개로 분해"""
    def rewriter(expr, matches):
        input_expr = matches["input"]
        
        # 첫 번째 5x5 MaxPool
        first_pool = relax.op.nn.max_pool2d(
            input_expr, 
            pool_size=[5, 5], 
            padding=[2, 2, 2, 2]
        )
        
        # 두 번째 5x5 MaxPool
        second_pool = relax.op.nn.max_pool2d(
            first_pool,
            pool_size=[5, 5],
            padding=[2, 2, 2, 2]
        )
        
        return second_pool
    return rewriter

이 패턴은 pool_size=[9, 9]padding=[4, 4, 4, 4]를 가진 MaxPool 연산을 감지하고, 동일한 receptive field를 유지하면서 두 개의 5x5 MaxPool로 분해합니다.

3. PatternMatchingRewriter 적용

정의된 패턴과 rewriter를 사용하여 실제 변환을 수행합니다.

from tvm.relax.dpl import PatternMatchingRewriter

def apply_transformations(mod):
    """모듈에 MISH 및 MaxPool 변환 적용"""
    
    # MISH 변환
    mish_pattern, mish_annotations = create_mish_pattern()
    mish_rewriter = create_mish_rewriter()
    mish_rewriter_obj = PatternMatchingRewriter.from_pattern(
        mish_pattern, mish_rewriter
    )
    
    # MaxPool 분해
    maxpool_pattern, maxpool_annotations = create_large_maxpool_pattern()
    maxpool_rewriter = create_maxpool_decomposer()
    maxpool_rewriter_obj = PatternMatchingRewriter.from_pattern(
        maxpool_pattern, maxpool_rewriter
    )
    
    # 변환 순차적 적용
    transformed_mod = mish_rewriter_obj(mod)
    transformed_mod = maxpool_rewriter_obj(transformed_mod)
    
    return transformed_mod

변환은 순차적으로 적용됩니다. 먼저 MISH 패턴을 LeakyReLU로 변환한 후, 큰 MaxPool을 분해합니다.

변환 전/후 IR

변환 전

def @main(%data: Tensor[(1, 3, 32, 32), float32], %weight1: Tensor[(16, 3, 3, 3), float32]) {
  %0 = nn.conv2d(%data, %weight1, padding=[1, 1, 1, 1], kernel_size=[3, 3]);
  %1 = exp(%0);
  %2 = add(%1, 1f);
  %3 = log(%2);
  %4 = tanh(%3);
  %5 = multiply(%0, %4);
  nn.max_pool2d(%5, pool_size=[9, 9], padding=[4, 4, 4, 4])
}

변환 후

def @main(%data: Tensor[(1, 3, 32, 32), float32], %weight1: Tensor[(16, 3, 3, 3), float32]) {
  %0 = nn.conv2d(%data, %weight1, padding=[1, 1, 1, 1], kernel_size=[3, 3]);
  %1 = nn.leaky_relu(%0, alpha=0.1f);
  %2 = nn.max_pool2d(%1, pool_size=[5, 5], padding=[2, 2, 2, 2]);
  nn.max_pool2d(%2, pool_size=[5, 5], padding=[2, 2, 2, 2])
}

Relay IR 이후의 flow는 다음과 같습니다.

graph TD
    A[원본 모델] --> B[TVM Frontend]
    B --> C[Relay IR]
    C --> D[partition_for_vitis_ai]
    D --> E[PyXIR / XIR 변환]
    E --> F[Vitis AI DPU 컴파일러]
    F --> G[TVM Runtime Module]
    G --> H[OTF Quantization]
    H --> I[DPU 실행]

Language: English

  1. Vitis-AI Model Zoo: https://github.com/Xilinx/Vitis-AI/tree/master/model_zoo 

  2. Vitis AI 지원 연산자: https://docs.amd.com/r/en-US/ug1414-vitis-ai/Currently-Supported-Operators  2

  3. Vitis-AI/third_party/tvm: https://github.com/Xilinx/Vitis-AI/tree/master/third_party/tvm 

  4. Andrew Ekblad, Trupti Mahendrakar,Ryan White, Markus Wilde, Isaac Silver, and Brooke Wheeler, “Resource-constrained FPGA Design for Satellite Component Feature Extraction”, in IEEE Aerospace Conference, BigSky, MT, USA, 2023 

  5. A. L. Mass, A. Y. Hannun and A. Y. Ng, “Rectifier Nonlinearities Improve Neural Network Acoustic Models”, in International Conference on Machine Learning, Atlanta, 2013