제9부 AI·엣지 응용
9.3 TensorFlow Lite·ONNX Runtime GPU 가속 추론
9.2은 App Lab의 Brick을 통해 모델을 배포하는 방법을 다뤘다. 이 방식은 간편하지만 내부가 블랙박스다 — 어떤 런타임이, 어떤 백엔드(CPU/GPU/DSP)로 추론하는지 사용자가 직접 제어할 수 없다. 이 장은 그 블랙박스를 열고, SSH로 접속한 Debian 셸(5.2 참고)에서 Python으로 직접 TensorFlow Lite(LiteRT)와 ONNX Runtime을 호출하는 방법을 다룬다. 목표는 "GPU 가속이 된다/안 된다"를 독자가 스스로 검증할 수 있게 하는 것이다.
9.3.1 "GPU 가속"이라는 말이 가리키는 두 가지 경로
- 경로 1 (9.2): App Lab Brick 내부에서 자동으로 처리 — 사용자가 런타임을 선택하지 않는다.
- 경로 2 (이 장): Python 스크립트에서
tflite/onnxruntime패키지를 직접 import해서, 어떤 실행 provider(delegate)를 쓸지 명시적으로 고른다.
이 장은 경로 2를 다룬다. App Lab 없이 순수 Debian 환경에서 실행하므로, 앱을 App Lab GUI에 등록하지 않고 SSH 터미널에서 바로 스크립트를 실행해도 된다.
9.3.2 TensorFlow Lite(LiteRT) — 공식 문서가 실제로 말하는 것
Google의 LiteRT(TensorFlow Lite의 현재 이름) GPU delegate 공식 문서(developers.google.com/edge/litert/performance/gpu, 집필 시점 실제 확인)는 Android용 가이드와 iOS용 가이드만 제공한다. 리눅스·임베디드 리눅스·aarch64에 대한 공식 GPU delegate 문서는 이 페이지에서 찾지 못했다.
이는 중요한 함정이다. 블로그·튜토리얼 중에는 "TFLite GPU delegate로 서브-100ms 추론"이라고 소개하는 글이 있지만, 이런 수치는 대개 Android 기기(공식 지원 플랫폼) 기준이다. UNO Q의 Debian 리눅스 환경에 그대로 적용된다고 가정하면 안 된다 — 벤더·커뮤니티 주장과 실측을 구분해야 한다는 이 책의 원칙(BOOK.md BK-04)이 특히 이 대목에 해당한다.
9.3.2.1 CPU 경로부터 — tflite-runtime / ai-edge-litert
# SSH로 UNO Q Debian에 접속한 뒤 (5.2 참고)
python3 -m venv ~/tflite-env
source ~/tflite-env/bin/activate
pip install tflite-runtime # 또는 최신 패키지명 ai-edge-litert — 설치 시점에 PyPI에서 확인
⚠️ 확인 필요: tflite-runtime PyPI 패키지는 2023년 10월 배포된 2.14.0 버전이 최신으로 확인됐다(집필 시점). Google이 LiteRT로 브랜드를 바꾸며 후속 패키지(ai-edge-litert 등)를 냈을 가능성이 있으므로, 설치 전에 pip index versions tflite-runtime이나 PyPI 페이지에서 최신 패키지명을 직접 확인하기 바란다.
import time
import numpy as np
from tflite_runtime.interpreter import Interpreter # 패키지명은 위 확인 필요 사항 참고
interpreter = Interpreter(model_path="model.tflite")
interpreter.allocate_tensors()
input_detail = interpreter.get_input_details()[0]
output_detail = interpreter.get_output_details()[0]
dummy_input = np.zeros(input_detail["shape"], dtype=input_detail["dtype"])
# 워밍업 — 첫 실행은 모델 로드·메모리 할당 오버헤드가 섞여 느리다
interpreter.set_tensor(input_detail["index"], dummy_input)
interpreter.invoke()
N = 50
latencies_ms = []
for _ in range(N):
start = time.perf_counter()
interpreter.set_tensor(input_detail["index"], dummy_input)
interpreter.invoke()
_ = interpreter.get_tensor(output_detail["index"])
latencies_ms.append((time.perf_counter() - start) * 1000)
import statistics
print(f"CPU 평균 {statistics.mean(latencies_ms):.2f}ms, "
f"중앙값 {statistics.median(latencies_ms):.2f}ms, "
f"최대 {max(latencies_ms):.2f}ms")
이것이 이 장의 신뢰할 수 있는 기준선(baseline)이다 — CPU 실행은 특별한 드라이버 없이 대부분의 aarch64 리눅스 환경에서 동작한다.
9.3.2.2 GPU delegate를 시도하려면
공식 문서가 리눅스를 다루지 않으므로, aarch64 Debian에서 TFLite GPU delegate(OpenCL/OpenGL ES 백엔드)를 쓰려면 대개 TensorFlow를 소스에서 GPU delegate·OpenCL 지원을 켜서 직접 빌드해야 한다. 이는 고급·자기 책임 경로다. 시도한다면:
try:
from tflite_runtime.interpreter import load_delegate
gpu_delegate = load_delegate("libtensorflowlite_gpu_delegate.so")
interpreter = Interpreter(model_path="model.tflite", experimental_delegates=[gpu_delegate])
print("GPU delegate 로드 성공")
except (ValueError, OSError) as e:
print(f"GPU delegate 로드 실패, CPU로 폴백: {e}")
interpreter = Interpreter(model_path="model.tflite")
이 코드가 UNO Q에서 실제로 libtensorflowlite_gpu_delegate.so를 찾아 로드에 성공하는지는 이 책 집필 시점 기준 검증하지 못했다. 실패하면 예외를 포착해 CPU로 자동 폴백하도록 구현하는 것이 실전에서 안전하다.
9.3.3 ONNX Runtime — CPU 기본 경로
ONNX Runtime은 CPUExecutionProvider가 aarch64 리눅스에서 기본으로 잘 동작하는 편이다.
pip install onnxruntime
import time
import numpy as np
import onnxruntime as ort
session = ort.InferenceSession("model.onnx", providers=["CPUExecutionProvider"])
input_name = session.get_inputs()[0].name
input_shape = session.get_inputs()[0].shape
dummy_input = np.random.rand(*[d if isinstance(d, int) else 1 for d in input_shape]).astype(np.float32)
# 워밍업
session.run(None, {input_name: dummy_input})
N = 50
latencies_ms = []
for _ in range(N):
start = time.perf_counter()
session.run(None, {input_name: dummy_input})
latencies_ms.append((time.perf_counter() - start) * 1000)
import statistics
print(f"ONNX Runtime CPU 평균 {statistics.mean(latencies_ms):.2f}ms")
9.3.4 ONNX Runtime QNN — Qualcomm 가속 경로와 현재 상태
ONNX Runtime은 Qualcomm SoC 가속을 위한 별도 패키지 onnxruntime-qnn을 제공한다. PyPI 페이지(집필 시점 실제 확인)에 따르면 이 패키지는 aarch64 리눅스용 wheel을 배포하며, "Qualcomm AI Runtime SDK(QAIRT)를 통해 Snapdragon SoC에서 고성능 AI 추론"을 지원한다고 설명한다.
pip install onnxruntime-qnn
import onnxruntime as ort
providers = ["QNNExecutionProvider", "CPUExecutionProvider"] # 실패 시 CPU로 자동 폴백되도록 순서 지정
try:
session = ort.InferenceSession(
"model.onnx",
providers=providers,
provider_options=[{"backend_path": "libQnnHtp.so"}, {}],
)
print("사용 중인 provider:", session.get_providers())
except Exception as e:
print(f"QNN provider 초기화 실패: {e}")
session = ort.InferenceSession("model.onnx", providers=["CPUExecutionProvider"])
⚠️ 커뮤니티 방법(비공식) — 실패 사례 보고됨. Arduino 포럼 스레드 "AI Inference on UNO Q Adreno 702 GPU without App Lab"(forum.arduino.cc, 2026-06-22 게시, 이 책 집필 시점까지 응답 없음)에서 한 사용자가 정확히 이 방식(onnxruntime-qnn으로 MobileNetV2 추론)을 시도했다가 QNN_BACKEND_ERROR_CANNOT_INITIALIZE 오류를 만났고, 원인을 "Adreno 702용 Qualcomm 독점 OpenCL/QNN 드라이버가 보드에 없음"으로 추정했다. 관련 스레드("Status of Qualcomm QNN / TFLite delegate / Edge Impulse QNN acceleration", forum.arduino.cc, 2026-06-08 게시, 마찬가지로 응답 없음)도 libQnnTFLiteDelegate.so 등 QNN 유저랜드 라이브러리가 공식 이미지에 없다고 보고한다. 이 문제의 배경과 대안 경로는 9.4에서 더 깊이 다룬다.
9.3.5 실험 설계 — CPU 대비 속도를 직접 재는 법
이 책은 "GPU 가속으로 몇 ms가 나온다"는 구체적 수치를 제시하지 않는다. 위에서 본 대로 (1) 공식 GPU delegate 문서가 리눅스를 다루지 않고, (2) QNN 경로는 커뮤니티에서 초기화 실패가 보고됐기 때문에, 실제로 어떤 가속이 얼마나 작동하는지는 보드 개체·이미지 버전·드라이버 상태에 따라 달라질 수 있다. 대신 독자가 스스로 측정할 수 있는 절차를 제시한다.
- 동일 모델, 동일 입력으로 CPU·(가능하다면) GPU·(가능하다면) QNN 경로를 모두 실행한다.
- 워밍업 실행을 반드시 버린다 — 첫 실행은 모델 로드·메모리 할당·JIT 컴파일 비용이 섞여 있다.
- 최소 50회 이상 반복하고 평균·중앙값·p95(상위 5% 지연)를 함께 기록한다 — 평균만 보면 이상값에 왜곡되기 쉽다.
- 열 상태를 기록한다 — 장시간 연속 추론 시 SoC 온도가 상승해 스로틀링이 걸릴 수 있다(2.4 전원 설계, 5.5 전력·성능 관리 참고).
cat /sys/class/thermal/thermal_zone*/temp등으로 온도를 함께 로깅하면 원인 분석에 도움이 된다. - 결과를 표로 남긴다 — 모델·해상도·배치 크기·provider별로 비교표를 만들면 나중에 재현·공유하기 쉽다.
import json
def benchmark(run_fn, n=50, label="unknown"):
# 워밍업
run_fn()
latencies = []
for _ in range(n):
start = time.perf_counter()
run_fn()
latencies.append((time.perf_counter() - start) * 1000)
latencies.sort()
result = {
"label": label,
"n": n,
"mean_ms": statistics.mean(latencies),
"median_ms": statistics.median(latencies),
"p95_ms": latencies[int(n * 0.95) - 1],
"fps_from_median": 1000 / statistics.median(latencies),
}
print(json.dumps(result, indent=2))
return result
9.3.6 함정
- 양자화(quantization): int8 양자화 모델은 CPU에서도 크게 빨라질 수 있다. GPU/QNN 가속 여부와 별개로, 모델 자체를 int8로 바꾸는 것이 UNO Q에서는 더 확실한 최적화일 수 있다.
- 모델 변환 손실:
.tflite↔.onnx변환 도구는 연산자 지원 범위가 다르다. 변환 후 반드시 출력값을 원본 프레임워크와 대조해 정확도 손실이 없는지 확인한다. - provider 순서: ONNX Runtime의
providers리스트는 앞에서부터 시도하고 실패하면 다음으로 넘어간다.QNNExecutionProvider를 앞에 두고CPUExecutionProvider를 뒤에 두면 위 코드처럼 자동 폴백이 되지만, 조용히 CPU로 떨어졌는데 로그를 확인하지 않으면 "가속이 되고 있다"고 착각하기 쉽다.session.get_providers()로 실제 사용 provider를 항상 확인한다. - App Lab Brick과의 차이: 이 장의 방식은 App Lab을 거치지 않으므로, 9.2의 Brick·Logic 플로우와 통합되지 않는다. App Lab 앱 안에서 커스텀 런타임을 쓰고 싶다면 Brick의 Python 코드 내부에서 이 장의 코드를 호출하는 식으로 결합해야 한다(App Lab 앱 구조는 4.2 참고).
9.3.7 다음 장으로
- 9.4: Adreno 702 GPU에 App Lab 없이 직접 접근하려는 커뮤니티 시도 — QNN 실패의 배경과 Mesa 오픈소스 스택 대안.
- 9.5: 카메라 비전 예제에 이 장의 벤치마크 기법을 적용해 실시간 FPS를 측정하는 방법.
- 2.4 / 5.5: 전원·전력 관리 — 장시간 추론 시 스로틀링 이해에 필요.