Published on

PT2E+ExecuTorch 양자화로 모바일 배포하기

Authors

모바일 온디바이스 추론은 모델 정확도만큼이나 지연시간, 배터리, 바이너리 크기, 메모리가 승패를 가릅니다. 서버에서야 GPU와 넉넉한 RAM으로 버틸 수 있지만, 모바일은 CPU 중심(혹은 제한된 NPU)이고 스레딩/캐시/발열 제약이 큽니다. 그래서 결국 양자화(INT8) 가 가장 높은 ROI를 주는 최적화가 되는 경우가 많습니다.

이 글에서는 PyTorch의 최신 내보내기 경로인 PT2E(PyTorch 2 Export) 와 모바일 런타임인 ExecuTorch 를 엮어, 양자화된 모델을 모바일로 배포하는 전체 흐름을 한 번에 정리합니다. 특히 “학습된 모델을 어떻게 INT8로 만들고, 어떻게 모바일에서 돌릴지”를 단계별로 설명합니다.

참고: 모바일 앱 배포 파이프라인은 빌드/의존성 이슈가 잦습니다. iOS 쪽 빌드가 자주 흔들린다면 Flutter iOS 빌드 실패? CocoaPods Podfile 7가지처럼 원인 체크리스트를 갖고 접근하는 습관이 도움이 됩니다.

PT2E와 ExecuTorch를 같이 쓰는 이유

왜 PT2E인가

기존 TorchScript는 여전히 쓰이지만, PyTorch 2.x 이후에는 torch.export를 중심으로 그래프를 더 안정적으로 고정하고, 백엔드(컴파일러/런타임)가 최적화하기 쉬운 형태로 넘기는 방향이 강화되었습니다.

PT2E의 장점은 다음과 같습니다.

  • 동적 파이썬 코드를 실행 단계에서 걷어내고, 비교적 정적인 그래프로 내보낼 수 있음
  • 양자화/최적화 패스가 적용될 표준화된 표현을 얻기 쉬움
  • 백엔드별 제약(지원 연산, 레이아웃)을 더 이른 단계에서 드러내고 수정 가능

왜 ExecuTorch인가

ExecuTorch는 모바일/엣지에 최적화된 PyTorch 런타임입니다.

  • 모바일에서 필요한 최소 런타임을 지향(바이너리 사이즈/의존성 관리)
  • 백엔드(예: XNNPACK 등)로 CPU 최적화 실행
  • 모델을 ExecuTorch 전용 포맷으로 패키징해 앱에 포함

요약하면, PT2E로 “내보내기/변환에 유리한 그래프”를 만들고, **ExecuTorch로 “모바일에서 가볍게 실행”**하는 조합입니다.

전체 파이프라인 개요

실무에서 가장 흔한 흐름은 아래와 같습니다.

  1. PyTorch 모델 준비(가능하면 eval() 고정, 입력 shape 정책 결정)
  2. PT2E로 export (torch.export.export)
  3. 양자화 준비(관측자 삽입, 캘리브레이션 데이터 준비)
  4. 양자화 변환(INT8) 및 검증(정확도/속도)
  5. ExecuTorch로 패키징(프로그램/아티팩트 생성)
  6. Android/iOS 앱에서 ExecuTorch 런타임으로 로드 및 추론

이 중 실패가 자주 나는 구간은 보통 2~4번(그래프 고정 및 양자화)과 6번(모바일 빌드/ABI/스레딩/메모리)입니다.

예제 모델과 입력 정책

모바일 배포에서는 입력 shape를 고정하는 편이 최적화에 유리합니다. 예를 들어 이미지 분류면 NCHW = 1x3x224x224로 고정하고, 전처리를 앱에서 동일하게 재현합니다.

아래는 간단한 CNN 예시입니다.

import torch
import torch.nn as nn
import torch.nn.functional as F

class SmallCNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 16, 3, stride=2, padding=1)
        self.conv2 = nn.Conv2d(16, 32, 3, stride=2, padding=1)
        self.fc = nn.Linear(32 * 56 * 56, 10)

    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = F.relu(self.conv2(x))
        x = torch.flatten(x, 1)
        return self.fc(x)

model = SmallCNN().eval()
example = torch.randn(1, 3, 224, 224)

PT2E로 export하기

PT2E의 핵심은 torch.export.exportExportedProgram을 얻는 것입니다.

import torch

# PyTorch 2.x
exported = torch.export.export(model, (example,))

# 그래프 확인(디버깅에 유용)
print(exported.graph_module)

export 단계에서 자주 막히는 포인트

  • 데이터 의존 분기: 텐서 값에 따라 if 분기하는 코드
  • 지원되지 않는 연산: 특정 커스텀 오퍼레이터, 일부 동적 shape 연산
  • 학습 모드 레이어: Dropout, BatchNormeval()로 고정되지 않은 경우

이때는 모델의 forward를 “내보내기 친화적”으로 리팩터링해야 합니다.

양자화 전략 선택: PTQ vs QAT

모바일 INT8을 목표로 할 때 선택지는 크게 두 가지입니다.

  • PTQ(Post-Training Quantization): 학습 없이 캘리브레이션 데이터만으로 양자화. 빠르지만 정확도 손실이 있을 수 있음.
  • QAT(Quantization-Aware Training): 학습 과정에 양자화 노이즈를 반영. 시간이 들지만 정확도 방어에 유리.

실무 팁:

  • 분류/간단 CNN은 PTQ로도 충분한 경우가 많음
  • 탐지/세그/트랜스포머 계열은 QAT가 필요해지는 경우가 잦음

이 글에서는 PTQ 중심으로 흐름을 잡고, 마지막에 QAT 전환 포인트를 짚겠습니다.

PT2E 기반 양자화(관측자 + 캘리브레이션)

PyTorch 양자화는 버전/백엔드에 따라 API가 다소 변합니다. 핵심 개념은 동일합니다.

  • prepare: 관측자(observer) 삽입
  • calibrate: 대표 데이터로 통계 수집(min/max 등)
  • convert: INT8 연산으로 치환

아래 코드는 “구조를 이해하기 위한” 예시이며, 실제 프로젝트에서는 사용하는 PyTorch 버전의 torch.ao.quantization 문서를 확인해 API 이름을 맞추는 것이 안전합니다.

import torch
from torch.ao.quantization import get_default_qconfig
from torch.ao.quantization.quantize_fx import prepare_fx, convert_fx

# FX 기반 PTQ 예시(백엔드에 따라 qconfig 달라짐)
qconfig = get_default_qconfig("qnnpack")

# exported.graph_module를 직접 쓰거나, 원본 model을 FX로 준비할 수 있음
prepared = prepare_fx(model, {"": qconfig}, example_inputs=(example,))

# 캘리브레이션: 대표 데이터(수십~수백 배치 권장)
with torch.inference_mode():
    for _ in range(100):
        x = torch.randn(1, 3, 224, 224)
        prepared(x)

quantized_model = convert_fx(prepared)
quantized_model.eval()

# 정확도/출력 비교(간단 sanity)
with torch.inference_mode():
    y_fp32 = model(example)
    y_int8 = quantized_model(example)

print((y_fp32 - y_int8).abs().mean().item())

캘리브레이션 데이터의 품질이 정확도를 좌우한다

PTQ에서 가장 흔한 실패는 “캘리브레이션 데이터를 대충 랜덤으로 넣어서” 스케일이 망가지는 것입니다.

권장:

  • 실제 서비스 입력 분포를 대표하는 샘플 사용
  • 전처리(리사이즈/정규화)까지 포함해 동일하게 적용
  • 너무 적은 샘플로 끝내지 않기(레이어별 통계가 불안정)

ExecuTorch로 내보내기: 모바일 실행 아티팩트 만들기

ExecuTorch는 보통 “모델을 ExecuTorch가 로드 가능한 형태”로 변환하는 단계가 필요합니다. 프로젝트 구조/버전에 따라 CLI나 파이썬 API가 제공됩니다.

개념적으로는 아래 두 가지가 중요합니다.

  • 지원 백엔드 선택: CPU라면 XNNPACK 계열 최적화가 일반적
  • 연산 커버리지 확인: 양자화된 연산이 런타임에서 지원되는지

예시 형태(개념 코드):

# 개념 예시입니다. 실제 API는 ExecuTorch 버전에 따라 다릅니다.
# 핵심은 "exported program" 또는 "quantized graph"를
# executorch 프로그램으로 패키징한다는 점입니다.

# from executorch import ...

# et_program = executorch.export(quantized_model, example_inputs=(example,))
# et_program.save("model.et")

실무에서는 여기서 다음을 반드시 확인합니다.

  • 모델 파일 크기(대략 FP32 대비 2~4배 감소 기대)
  • 런타임 로딩 시간(앱 시작 성능)
  • 스레드 수에 따른 지연시간(빅코어/리틀코어 스케줄링)

Android에서 로드/추론(개념)

Android는 보통 AAR 또는 네이티브 라이브러리로 ExecuTorch 런타임을 포함하고, assets에 모델 아티팩트를 넣어 로드합니다.

의사 코드(개념):

// 개념 예시: 실제 클래스/메서드는 ExecuTorch Android API에 맞게 조정 필요

val modelBytes = assets.open("model.et").readBytes()
val module = ETModule.load(modelBytes)

val input = FloatArray(1 * 3 * 224 * 224) { 0.0f }
val output = module.forward(input)

모바일에서는 전처리 비용이 전체 지연시간의 상당 부분을 차지할 수 있습니다. 예를 들어 BitmapFloatArray로 바꾸는 과정에서 메모리 복사가 과도하게 발생하면, 모델이 빨라도 전체가 느립니다. 따라서 전처리도 다음을 고려하세요.

  • 가능한 한 네이티브 버퍼 재사용
  • RGB 변환/정규화를 SIMD 친화적으로 구현
  • 입력 텐서를 매번 새로 할당하지 않기

성능 검증 체크리스트(정확도/지연시간/메모리)

양자화 성공 여부는 “파일이 만들어졌다”가 아니라, 아래 지표로 판단해야 합니다.

1) 정확도 회귀

  • FP32 대비 Top-1/Top-5 하락폭
  • 특정 클래스에서만 무너지는지(캘리브레이션 분포 문제 가능)

2) 지연시간

  • 워밍업 후 p50/p95 측정
  • 스레드 수(1, 2, 4)별 측정
  • big.LITTLE 환경에서 고정 클럭이 아니므로 반복 측정

3) 메모리

  • 로딩 시점 peak
  • 추론 중 peak
  • 입력/출력 버퍼 재사용 여부

이런 “운영 지표 기반 점검”은 서버에서도 동일합니다. 예를 들어 장애 시 빠르게 원인을 줄여나가는 방식은 EKS Pod CrashLoopBackOff? OOMKilled 5분 진단 같은 글에서 다루는 사고방식과 비슷하게 적용됩니다. 모바일도 결국 제한된 리소스에서 병목을 찾아 제거하는 게임입니다.

흔한 실패 패턴과 해결법

패턴 1: 특정 연산이 양자화되지 않아서 속도가 안 나온다

증상:

  • INT8로 변환했는데도 지연시간이 FP32와 비슷
  • 프로파일링해보면 일부 구간이 FP32로 남아 있음

대응:

  • 해당 연산이 백엔드에서 지원되는지 확인
  • 모델 구조를 “양자화 친화적”으로 변경(예: Conv+ReLU fuse 유도)
  • 레이어 단위로 양자화 제외/포함 정책을 조정

패턴 2: 캘리브레이션 후 정확도가 크게 떨어진다

대응:

  • 캘리브레이션 데이터 품질 개선(실데이터/실전처리)
  • per-tensor 대신 per-channel quantization 고려(특히 Conv weight)
  • PTQ로 안 되면 QAT로 전환

패턴 3: export 단계에서 그래프가 깨진다

대응:

  • 텐서 값 기반 분기 제거
  • 동적 shape를 줄이고 입력 규격을 고정
  • 커스텀 연산을 표준 연산 조합으로 치환

QAT로 넘어가야 하는 시점

다음 중 하나라도 해당하면 QAT를 고려하는 편이 좋습니다.

  • PTQ에서 정확도 하락이 비즈니스 임계치 이상
  • 모델이 트랜스포머/어텐션 중심이고 activation 분포가 민감
  • 특정 레이어가 outlier에 매우 취약(클리핑/스케일링으로 해결 어려움)

QAT는 학습 파이프라인에 양자화 스텁을 넣고, 학습률/에폭을 재조정해야 하므로 비용이 큽니다. 하지만 모바일 배포가 목표라면, “PTQ로 시도하고 안 되면 QAT” 순서가 현실적입니다.

정리: 모바일 배포 관점에서의 권장 접근

  • 입력 정책을 먼저 결정하고(1x3x224x224 같은 고정 shape), 전처리까지 포함해 재현 가능하게 만든다.
  • PT2E로 export해 그래프를 고정하고, 지원 연산/동작을 조기에 확인한다.
  • PTQ(INT8)로 빠르게 성능 이득을 얻되, 캘리브레이션 데이터 품질에 투자한다.
  • ExecuTorch로 패키징해 실제 기기에서 지연시간/메모리/정확도를 재검증한다.
  • PTQ로 정확도가 안 나오면 QAT로 전환한다.

모바일 온디바이스 ML은 “모델”만이 아니라 “런타임/전처리/스레딩/메모리”가 모두 합쳐진 시스템 문제입니다. PT2E+ExecuTorch 조합은 그 시스템을 PyTorch 생태계 안에서 비교적 일관된 방식으로 구성할 수 있게 해주는 도구들이고, 양자화는 그중 가장 강력한 레버리지입니다.