클라우드를 끊어도 AI가 돌아간다 — Edge AI 온디바이스 배포 파이프라인 구현
저도 처음엔 "그냥 서버에서 추론하면 되지 않나?"라는 생각으로 시작했습니다. 그런데 실무에서 AR 프로토타입 레이턴시 이슈를 맞닥뜨리고 나서 생각이 완전히 바뀌었습니다. 클라우드 왕복에 걸리는 수백 밀리초가 AR 기기에선 멀미를 유발하고, 자율주행차에선 사고로 이어질 수 있습니다. 비행기 모드에서도 Siri가 대답하고, Meta Quest 3가 네트워크 없이 손 동작을 추적하고, 공장 센서가 인터넷 없이 0.1초 만에 이상을 탐지하는 것이 바로 이 구조 덕분입니다.
이 글에서는 Edge AI 온디바이스 추론이 어떤 구조로 동작하고, Android 앱부터 크로스 플랫폼 Python 코드까지 실제 배포 파이프라인을 어떻게 구성하는지 코드와 함께 살펴봅니다. 이 글을 다 읽으면 INT8 양자화 모델을 Android 앱 또는 ONNX Runtime으로 실행하는 추론 파이프라인을 직접 구성할 수 있습니다.
2025년 기준 글로벌 엣지 AI 시장은 249억 달러 규모이며 2033년까지 약 1,187억 달러(CAGR 21.7%)에 이를 것으로 전망됩니다(Grand View Research). 전체 AI 컴퓨팅에서 추론 워크로드 비율은 이미 50%를 넘어섰고(CEVA 2025 Edge AI Report), Llama 3.2 1B, Gemma 3 270M 같은 서브-빌리언 모델들이 실제 태스크를 처리할 수 있는 수준에 도달하면서 온디바이스 추론은 "연구 영역"을 벗어나 "현업 엔지니어링"의 문제가 되었습니다.
핵심 개념
온디바이스 추론이란 정확히 무엇인가
클라우드 기반 AI 파이프라인은 단순합니다. 데이터를 서버로 보내고, 서버가 추론하고, 결과를 돌려받습니다. 온디바이스 추론은 이 과정을 로컬 하드웨어에서 완결합니다. 데이터가 디바이스 밖으로 나가지 않습니다.
엣지(Edge) 의 범위는 맥락에 따라 다릅니다. 스마트폰·태블릿 같은 사용자 단말부터 공장 게이트웨이, 자율주행차 온보드 컴퓨터, 웨어러블까지 모두 포함합니다. 공통점은 "데이터가 생성되는 장소에서 추론까지 완결한다"는 것입니다.
세 가지 구성 요소가 맞물려 동작합니다.
| 구성 요소 | 역할 | 대표 기술 |
|---|---|---|
| 경량화 모델 | 클라우드용 대형 모델을 엣지에서 실행 가능한 크기로 축소 | 양자화, 가지치기, 지식 증류 |
| 추론 런타임 | 디바이스별 최적화 실행 엔진 | LiteRT, ONNX Runtime, ExecuTorch, Core ML |
| 하드웨어 가속기 | AI 연산 전용 칩으로 속도·전력 효율 확보 | NPU, GPU, DSP |
모델 경량화의 세 가지 축
클라우드 모델을 그대로 디바이스에 올리는 건 불가능합니다. RAM이 수백 MB~수 GB밖에 안 됩니다. 경량화 파이프라인을 거쳐야 합니다.
양자화(Quantization) 는 가장 효과적인 방법입니다. 모델 가중치의 정밀도를 FP32에서 INT8로 줄이면 모델 크기가 약 4배 줄고, INT4까지 내리면 최대 8배까지 줄어듭니다. 연산 속도가 빨라지고, 메모리 대역폭 요구도 낮아집니다.
import tensorflow as tf
converter = tf.lite.TFLiteConverter.from_saved_model("saved_model/")
# Post-Training Quantization (PTQ) 적용
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.target_spec.supported_types = [tf.int8]
# 대표 데이터셋으로 범위 보정 (INT8 정적 양자화)
def representative_dataset():
for data in calibration_data:
yield [data.astype("float32")]
converter.representative_dataset = representative_dataset
tflite_model = converter.convert()
with open("model_int8.tflite", "wb") as f:
f.write(tflite_model)PTQ vs QAT: Post-Training Quantization(PTQ)은 훈련 후 변환하는 방식으로 빠르게 적용 가능합니다. Quantization-Aware Training(QAT)은 훈련 중에 양자화를 시뮬레이션해 정확도 손실을 최소화합니다. 정확도가 중요한 태스크라면 QAT를 권장합니다.
가지치기(Pruning) 는 불필요한 가중치를 제거합니다. 구조적 가지치기(Structured Pruning)는 뉴런이나 레이어 단위로 제거하기 때문에 범용 하드웨어에서도 실제 속도 향상으로 이어집니다. 비구조적 가지치기는 행렬이 희소해지는 것뿐이라 전용 하드웨어 지원 없이는 체감 효과가 작습니다.
지식 증류(Knowledge Distillation) 는 대형 교사 모델의 출력 분포를 소형 학생 모델이 따라 배우도록 훈련하는 방식입니다. 단순히 레이블로 훈련하는 것보다 작은 모델에서 더 높은 정확도를 끌어낼 수 있습니다.
import torch
import torch.nn.functional as F
def distillation_loss(student_logits, teacher_logits, labels, temperature=4.0, alpha=0.7):
# KL divergence: 두 확률 분포 사이의 거리를 측정하는 함수입니다.
# 교사 모델의 소프트 확률 분포를 학생이 모방하도록 유도합니다.
soft_loss = F.kl_div(
F.log_softmax(student_logits / temperature, dim=1),
F.softmax(teacher_logits / temperature, dim=1),
reduction="batchmean"
) * (temperature ** 2)
# 하드 타겟 손실: 실제 레이블로 학습
hard_loss = F.cross_entropy(student_logits, labels)
return alpha * soft_loss + (1 - alpha) * hard_loss복합 파이프라인(가지치기 → 양자화 → 증류)을 순차 적용하면 큰 폭의 경량화가 가능합니다. Promwad의 분석에 따르면 대표적인 이미지 분류 벤치마크에서 파라미터 74% 감소, 정확도 손실 3% 이내의 결과가 달성된 사례가 있으며(AI Model Compression — Promwad), 허용 가능한 손실 수준은 태스크마다 사전에 정의해 두는 게 중요합니다.
추론 런타임 선택
솔직히 이게 제일 헷갈리는 부분이었습니다. 런타임이 너무 많아서요.
| 런타임 | 최적 대상 | 핵심 특징 |
|---|---|---|
| LiteRT (구 TFLite) | Android, 임베디드 Linux | NNAPI·GPU 델리게이트로 NPU 활용 |
| Core ML | iOS, macOS | Apple Neural Engine 완전 활용, Swift 네이티브 |
| ONNX Runtime | 크로스 플랫폼, 브라우저 | 프레임워크 중립, WASM으로 브라우저 추론도 가능 |
| ExecuTorch | PyTorch 생태계 | Meta가 Instagram·WhatsApp에 실제 적용 중 |
| llama.cpp | 데스크탑, 임베디드 LLM | CPU 최적화, GGUF 포맷 기반 |
플랫폼이 정해져 있다면 선택이 쉬워집니다. iOS라면 Core ML, Android 폭넓은 지원이 필요하다면 LiteRT, PyTorch로 훈련했고 여러 백엔드를 지원해야 한다면 ExecuTorch가 현실적인 선택입니다. 아래 예시에서는 Android(예시 1)와 크로스 플랫폼 Python(예시 2, 3)을 각각 다룹니다.
실전 적용
예시 1: Android 앱에서 LiteRT로 이미지 분류
Android 앱을 개발하고 있다면 이 예시부터 보시면 좋습니다. iOS 개발자라면 Core ML API 구조가 유사하므로 개념만 참고하고 예시 2로 넘어가셔도 됩니다.
카메라 피드를 실시간으로 분류하면서 NNAPI를 통해 기기 NPU를 활용하는 구조입니다. 처음에 NnApiDelegate 없이 그냥 돌렸다가 추론 시간이 기대치의 3배 이상 나왔던 기억이 있는데, 델리게이트 한 줄이 이렇게 차이를 낸다는 게 직접 겪어봐야 체감이 됩니다.
import org.tensorflow.lite.Interpreter
import org.tensorflow.lite.nnapi.NnApiDelegate
import android.content.res.AssetManager
import android.graphics.Bitmap
import java.io.FileInputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.MappedByteBuffer
import java.nio.channels.FileChannel
class EdgeInferenceEngine(private val assetManager: AssetManager, modelFileName: String) {
private val nnApiDelegate = NnApiDelegate()
private val interpreter: Interpreter
private val inputSize = 224
private val numClasses = 1000
init {
val options = Interpreter.Options().apply {
addDelegate(nnApiDelegate)
// NPU 폴백 시 CPU 멀티스레드로 처리
setNumThreads(4)
}
interpreter = Interpreter(loadModelFile(modelFileName), options)
}
fun classify(bitmap: Bitmap): FloatArray {
val inputBuffer = preprocessImage(bitmap)
val outputBuffer = Array(1) { FloatArray(numClasses) }
interpreter.run(inputBuffer, outputBuffer)
return outputBuffer[0]
}
// NnApiDelegate는 AutoCloseable을 구현하므로 반드시 해제해야 합니다.
// Activity나 Fragment의 onDestroy()에서 호출하시면 됩니다.
fun close() {
interpreter.close()
nnApiDelegate.close()
}
private fun loadModelFile(fileName: String): MappedByteBuffer {
val fileDescriptor = assetManager.openFd(fileName)
val inputStream = FileInputStream(fileDescriptor.fileDescriptor)
return inputStream.channel.map(
FileChannel.MapMode.READ_ONLY,
fileDescriptor.startOffset,
fileDescriptor.declaredLength
)
}
private fun preprocessImage(bitmap: Bitmap): ByteBuffer {
val resized = Bitmap.createScaledBitmap(bitmap, inputSize, inputSize, true)
// allocateDirect: JVM 힙 외부에 메모리를 할당해 JNI 복사 비용을 없앱니다.
// nativeOrder(): 디바이스 CPU의 바이트 순서(엔디안)에 맞춥니다.
val buffer = ByteBuffer.allocateDirect(4 * 3 * inputSize * inputSize)
buffer.order(ByteOrder.nativeOrder())
val pixels = IntArray(inputSize * inputSize)
resized.getPixels(pixels, 0, inputSize, 0, 0, inputSize, inputSize)
for (pixel in pixels) {
// 훈련 시 사용한 ImageNet 정규화 파라미터와 반드시 일치해야 정확도가 나옵니다.
buffer.putFloat(((pixel shr 16 and 0xFF) / 255.0f - 0.485f) / 0.229f)
buffer.putFloat(((pixel shr 8 and 0xFF) / 255.0f - 0.456f) / 0.224f)
buffer.putFloat(((pixel and 0xFF) / 255.0f - 0.406f) / 0.225f)
}
return buffer
}
}| 코드 포인트 | 설명 |
|---|---|
NnApiDelegate() |
Android NNAPI를 통해 디바이스 NPU/DSP에 연산을 위임. 없으면 CPU 폴백 |
loadModelFile() |
AssetManager로 APK 내 모델 파일을 메모리 맵으로 로드 |
allocateDirect + nativeOrder |
JNI 경계에서 복사 오버헤드 없이 네이티브 코드에 버퍼 전달 |
close() |
NnApiDelegate는 네이티브 리소스를 보유하므로 명시적 해제 필수 |
예시 2: Python으로 ONNX Runtime 크로스 플랫폼 추론
ML 엔지니어나 서버 사이드 개발자라면 이 예시가 가장 익숙한 시작점입니다. 라즈베리 파이부터 Windows 데스크탑까지 같은 코드로 동작합니다.
ONNX는 PyTorch나 TensorFlow 모델을 변환해서 어디서든 돌릴 수 있게 해주는 중간 포맷입니다.
from typing import Any
import onnxruntime as ort
import numpy as np
from PIL import Image
def create_session(model_path: str) -> ort.InferenceSession:
# 사용 가능한 프로바이더 자동 선택 (CUDA > CoreML > CPU 순)
providers = ort.get_available_providers()
print(f"사용 가능한 실행 프로바이더: {providers}")
session_options = ort.SessionOptions()
session_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
session_options.intra_op_num_threads = 4
return ort.InferenceSession(model_path, session_options, providers=providers)
def run_inference(session: ort.InferenceSession, image_path: str) -> np.ndarray:
# convert("RGB")를 명시하지 않으면 RGBA PNG나 흑백 이미지에서 shape mismatch가 납니다.
img = Image.open(image_path).convert("RGB").resize((224, 224))
input_data = np.array(img, dtype=np.float32) / 255.0
input_data = (input_data - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225]
input_data = np.transpose(input_data, (2, 0, 1)) # HWC → CHW
input_data = np.expand_dims(input_data, axis=0) # 배치 차원 추가
input_name = session.get_inputs()[0].name
outputs = session.run(None, {input_name: input_data})
return outputs[0]PyTorch 모델을 ONNX로 변환할 때 dynamic_axes 설정을 빠뜨리는 경우가 많은데, 배치 크기를 고정하면 나중에 다시 변환해야 하는 상황이 생깁니다.
import torch
def export_to_onnx(model: torch.nn.Module, save_path: str) -> None:
model.eval()
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(
model,
dummy_input,
save_path,
export_params=True,
opset_version=17,
input_names=["input"],
output_names=["output"],
dynamic_axes={"input": {0: "batch_size"}} # 배치 크기 동적 처리
)
print(f"ONNX 모델 저장됨: {save_path}")예시 3: 하이브리드 엣지-클라우드 라우팅
온디바이스와 클라우드 사이에서 어떻게 분기할지 고민하고 있다면 이 예시가 도움이 됩니다.
실무에서 자주 맞닥뜨리는 상황인데, 모든 추론을 디바이스에서 처리하는 게 항상 정답은 아닙니다. 간단한 요청은 디바이스에서, 복잡한 요청은 클라우드로 동적으로 분기하는 구조가 비용과 성능 사이에서 합리적인 균형점을 만들어 줍니다.
complexity_score는 라우팅 결정의 핵심 변수입니다. 입력 텍스트 길이, 모델 출력 엔트로피, 이전 추론 실패율, 쿼리 유형 등을 피처로 조합해 산출할 수 있습니다. 단순 규칙 기반으로 시작하다가 점차 학습 기반 분류기로 발전시키는 접근이 실무에서 현실적입니다.
from typing import Any
import asyncio
from dataclasses import dataclass
from enum import Enum
class InferenceTarget(Enum):
DEVICE = "device"
CLOUD = "cloud"
@dataclass
class InferenceRequest:
input_data: Any
# 0.0~1.0: 입력 길이, 출력 엔트로피, 이전 실패율 등을 조합해 산출
complexity_score: float
requires_privacy: bool
latency_budget_ms: float
class HybridInferenceRouter:
COMPLEXITY_THRESHOLD = 0.7
LATENCY_THRESHOLD_MS = 50.0
def __init__(self, local_model: Any, cloud_client: Any) -> None:
self.local_model = local_model
self.cloud_client = cloud_client
def route(self, request: InferenceRequest) -> InferenceTarget:
# 프라이버시 요구 시 무조건 로컬
if request.requires_privacy:
return InferenceTarget.DEVICE
# 레이턴시 예산이 타이트하면 로컬
if request.latency_budget_ms < self.LATENCY_THRESHOLD_MS:
return InferenceTarget.DEVICE
# 복잡도가 높으면 클라우드
if request.complexity_score > self.COMPLEXITY_THRESHOLD:
return InferenceTarget.CLOUD
return InferenceTarget.DEVICE
async def infer(self, request: InferenceRequest) -> Any:
target = self.route(request)
if target == InferenceTarget.DEVICE:
return await self.local_model.predict(request.input_data)
else:
return await self.cloud_client.predict(request.input_data)하이브리드 구성이 순수 클라우드 대비 에너지와 비용을 크게 줄여준다는 건 N-iX의 엣지 AI 트렌드 분석에서도 확인되는 방향입니다(Key edge AI trends — N-iX). 전부 디바이스에서 처리하지 않아도 됩니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 초저지연 | 네트워크 왕복 제거로 응답 시간 최대 70% 단축 — AR·자율주행처럼 20ms 이내가 필수인 시나리오에서 유일한 선택지 |
| 오프라인 동작 | 인터넷 연결 없이 완전 기능 유지 — 지하철, 비행기, 오지 현장에서도 동작 |
| 데이터 프라이버시 | 민감 데이터가 디바이스 외부로 전송되지 않음 — 의료·금융 영역에서 규제 대응에 유리 |
| 비용 절감 | 클라우드 API 호출 비용, 대역폭 비용 제거 |
| 신뢰성 | 서버 다운이나 네트워크 장애에 영향 없음 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 모델 성능 저하 | 양자화·가지치기·증류 복합 적용 시 정확도 2~4% 손실이 일반적 | 태스크별 허용 손실 기준을 사전 정의, QAT로 손실 최소화 |
| 하드웨어 파편화 | Android만 해도 수천 종의 칩셋 — NNAPI, Qualcomm QNN, MediaTek APU 각각 테스트 필요 | 주요 타깃 디바이스 목록을 좁히고 CI에 실기기 테스트 포함 |
| 메모리 제약 | 모델 파일 크기, 런타임 메모리, 피크 메모리를 별도 측정해야 함 | 배포 전 Profiler로 피크 메모리 확인 |
| 업데이트 복잡성 | 클라우드는 서버 재배포로 즉시 반영되지만, 온디바이스는 앱 업데이트나 OTA 파이프라인이 별도 필요 | 모델 파일과 앱 바이너리를 분리해 OTA 업데이트 가능하게 설계 |
| 보안 위협 | 디바이스에 모델 파일 저장 시 역공학 및 모델 추출 공격에 노출 | 모델 암호화, Secure Enclave 활용 고려 |
NPU(Neural Processing Unit): AI 행렬 연산에 특화된 전용 칩입니다. GPU가 범용 병렬 연산을 하는 것과 달리, NPU는 AI 추론에 필요한 연산만 극도로 최적화해 성능 대비 전력 효율이 훨씬 높습니다. 퀄컴 Hexagon, Apple Neural Engine, Samsung Exynos NPU가 대표적입니다.
실무에서 가장 흔한 실수
-
파일 크기만 보고 메모리를 안 재는 것 — 100MB 모델이 런타임에서 400MB를 쓰는 경우가 있습니다. 모델 파일 크기, 런타임 메모리, 피크 메모리는 반드시 별도로 측정하는 것을 권장합니다.
-
훈련 전처리와 추론 전처리가 달라지는 것 — 정규화 파라미터, 채널 순서(HWC vs CHW), 픽셀 값 범위(0
1 vs 0255)가 조금만 달라져도 정확도가 폭락합니다. 전처리 코드를 모델 변환 파이프라인에 포함시켜 버전을 함께 관리하는 것을 권장합니다. -
개발 머신 성능으로 성능을 판단하는 것 — M4 MacBook에서 50ms였던 추론이 3년 된 보급형 Android폰에서는 800ms가 나올 수 있습니다. 타깃 디바이스에서 직접 벤치마크를 돌려보는 것이 필수입니다.
마치며
온디바이스 추론이 단순히 "클라우드 모델을 줄여서 로컬에 올리는 것"이 아닌 이유는, 설계 철학 자체가 다르기 때문입니다. 클라우드 AI는 컴퓨팅 자원을 탄력적으로 확장하는 방향으로 설계합니다. 반면 온디바이스는 처음부터 메모리 한도, 전력 예산, 하드웨어 파편화를 제약 조건으로 받아들이고 그 안에서 최선의 정확도를 끌어내는 방향으로 접근합니다. 모델 아키텍처 선택, 경량화 순서, 런타임 선택, 하드웨어 테스트 전략, 모델 업데이트 파이프라인까지 — 클라우드 배포와는 다른 고려사항이 층층이 쌓여 있습니다. 이 제약 조건들을 설계 초반에 받아들이느냐, 배포 직전에 맞닥뜨리느냐가 프로젝트 성패를 가르는 경우가 많습니다.
지금 바로 시작해볼 수 있는 3단계:
-
모델 변환 체험: 익숙한 TensorFlow 또는 PyTorch 모델 하나를 골라
tf.lite.TFLiteConverter또는torch.onnx.export로 변환해보시면 좋습니다. 변환 전후 파일 크기 차이를 눈으로 확인하는 것만으로도 감이 빠르게 잡힙니다. -
벤치마크 측정: 변환된 모델을 ONNX Runtime이나 LiteRT로 로컬에서 실행해 레이턴시를 재보시면 됩니다.
ort.InferenceSession에 CPU 프로바이더만 붙여도 10줄이면 추론 파이프라인이 완성됩니다. -
양자화 적용 및 정확도 비교: PTQ로 INT8 양자화를 적용하고 원본 모델과 정확도를 비교해보시면 "얼마나 손실을 감수할 수 있는지"에 대한 직관이 생깁니다. Qualcomm AI Hub는 무료 티어에서 Snapdragon 타깃 프로파일링을 체험해볼 수 있고, Edge Impulse는 무료 플랜으로 IoT 타깃 배포까지 연결해볼 수 있습니다. 클라우드 플랫폼 없이 시작하고 싶다면
ort.SessionOptions의 프로파일링 옵션을 켜서 로컬에서 직접 측정하는 방법도 있습니다.
참고 자료
- Edge AI: The future of AI inference is smarter local compute | InfoWorld
- 2026 AI story: Inference at the edge, not just scale in the cloud | RD World Online
- On-Device LLMs in 2026: What Changed, What Matters, What's Next | Edge AI and Vision Alliance
- Key edge AI trends transforming enterprise tech in 2026 | N-iX
- Edge AI Market Size, Share & Trends | Grand View Research
- 2025 Edge AI Technology Report | CEVA
- AI Model Compression: Pruning and Quantization Strategies | Promwad
- Optimizing Your AI Model for the Edge | Qualcomm Developer Blog
- Efficient Inference at the Edge: Quantization, Pruning, and Knowledge Distillation | Uplatz
- Top 10 Edge AI Frameworks for 2025 | Huebits Blog
- ExecuTorch vs ONNX Runtime | Cactus Compute
- Optimizing Edge AI: A Comprehensive Survey | arXiv
- Empowering Edge Intelligence: A Comprehensive Survey on On-Device AI Models | arXiv
- 로컬 컴퓨팅으로 넘어가는 AI 추론 | CIO Korea
- 메타 엣지 디바이스용 AI 추론 프레임워크 '엑스큐토치 1.0' 공개 | CIO Korea