- Published on
PT2E+ExecuTorch 양자화로 모바일 배포하기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
모바일 온디바이스 추론은 모델 정확도만큼이나 지연시간, 배터리, 바이너리 크기, 메모리가 승패를 가릅니다. 서버에서야 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로 “모바일에서 가볍게 실행”**하는 조합입니다.
전체 파이프라인 개요
실무에서 가장 흔한 흐름은 아래와 같습니다.
- PyTorch 모델 준비(가능하면
eval()고정, 입력 shape 정책 결정) - PT2E로 export (
torch.export.export) - 양자화 준비(관측자 삽입, 캘리브레이션 데이터 준비)
- 양자화 변환(INT8) 및 검증(정확도/속도)
- ExecuTorch로 패키징(프로그램/아티팩트 생성)
- 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.export로 ExportedProgram을 얻는 것입니다.
import torch
# PyTorch 2.x
exported = torch.export.export(model, (example,))
# 그래프 확인(디버깅에 유용)
print(exported.graph_module)
export 단계에서 자주 막히는 포인트
- 데이터 의존 분기: 텐서 값에 따라
if분기하는 코드 - 지원되지 않는 연산: 특정 커스텀 오퍼레이터, 일부 동적 shape 연산
- 학습 모드 레이어:
Dropout,BatchNorm이eval()로 고정되지 않은 경우
이때는 모델의 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)
모바일에서는 전처리 비용이 전체 지연시간의 상당 부분을 차지할 수 있습니다. 예를 들어 Bitmap을 FloatArray로 바꾸는 과정에서 메모리 복사가 과도하게 발생하면, 모델이 빨라도 전체가 느립니다. 따라서 전처리도 다음을 고려하세요.
- 가능한 한 네이티브 버퍼 재사용
- 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 생태계 안에서 비교적 일관된 방식으로 구성할 수 있게 해주는 도구들이고, 양자화는 그중 가장 강력한 레버리지입니다.