온디바이스 AI 시스템의 성능 벤치마킹 방법론, 핵심 지표, 하드웨어별 성능 비교, 그리고 토큰 처리량과 메모리 사용을 최적화하는 기법을 다룹니다.
이전 8장에서는 온디바이스 AI 애플리케이션의 설계 패턴을 살펴보았습니다. 패턴을 적용하여 시스템을 구축했다면, 그 시스템이 실제로 요구사항을 충족하는지 정량적으로 검증해야 합니다. "체감상 빠르다"는 것과 "TTFT가 200ms 이하"라는 것은 전혀 다른 차원의 이야기입니다.
이번 장에서는 온디바이스 AI 시스템의 성능을 측정하는 핵심 지표, 벤치마킹 방법론, 하드웨어별 비교 데이터, 그리고 실전 최적화 기법을 다룹니다.
온디바이스 AI의 성능을 평가할 때 다음 여섯 가지 지표를 기본으로 측정합니다.
| 지표 | 단위 | 설명 | 목표 (7B 모델 기준) |
|---|---|---|---|
| Tokens/s (생성) | tok/s | 초당 생성 토큰 수 | 30+ tok/s |
| Tokens/s (프롬프트 처리) | tok/s | 프롬프트 평가 속도 | 200+ tok/s |
| TTFT | ms | 첫 번째 토큰까지의 시간 | 300ms 이하 |
| 메모리 사용량 | GB | 추론 중 최대 메모리 | 모델 크기 x 1.2 이하 |
| 모델 로딩 시간 | s | 모델을 메모리에 적재하는 시간 | 3s 이하 |
| 전력 소비 | W | 추론 중 평균 전력 | 디바이스별 상이 |
TTFT(Time To First Token)는 사용자 경험에 가장 직접적인 영향을 미치는 지표입니다. 사용자가 질문을 입력하고 첫 글자가 화면에 나타날 때까지의 시간을 의미하며, 이 값이 500ms를 넘으면 사용자는 지연을 체감하기 시작합니다.
llama.cpp에 내장된 llama-bench 도구는 온디바이스 LLM 벤치마킹의 사실상 표준입니다.
# 기본 벤치마크 실행
./llama-bench \
-m models/llama-3.2-7b-q4_k_m.gguf \
-p 512 \ # 프롬프트 토큰 수
-n 128 \ # 생성 토큰 수
-ngl 99 \ # GPU 레이어 수
-r 5 # 반복 횟수
# 여러 양자화 수준 비교
./llama-bench \
-m models/llama-3.2-7b-q4_k_m.gguf \
-m models/llama-3.2-7b-q5_k_m.gguf \
-m models/llama-3.2-7b-q8_0.gguf \
-p 256,512,1024 \
-n 128 \
-ngl 99 \
-r 3 \
-o json > benchmark_results.json벤치마크 결과의 신뢰성을 높이려면 최소 3회 이상 반복 측정하고, 첫 번째 실행은 워밍업으로 제외하는 것이 좋습니다. 또한 다른 프로세스의 간섭을 최소화하기 위해 백그라운드 애플리케이션을 종료한 상태에서 측정해야 합니다.
llama-bench로 측정할 수 없는 지표(TTFT, 메모리 피크 등)는 별도의 스크립트로 측정합니다.
import time
import json
import subprocess
import psutil
import requests
from dataclasses import dataclass, asdict
@dataclass
class BenchmarkResult:
model_name: str
quantization: str
prompt_tokens: int
generated_tokens: int
ttft_ms: float
generation_tok_s: float
peak_memory_gb: float
model_load_time_s: float
def measure_ttft(
server_url: str,
prompt: str,
) -> tuple[float, int]:
"""TTFT와 생성 토큰 수를 측정합니다."""
start = time.perf_counter()
first_token_time = None
token_count = 0
response = requests.post(
f"{server_url}/completion",
json={"prompt": prompt, "n_predict": 128, "stream": True},
stream=True,
)
for line in response.iter_lines():
if line:
if first_token_time is None:
first_token_time = time.perf_counter()
token_count += 1
ttft_ms = (first_token_time - start) * 1000 if first_token_time else -1
return ttft_ms, token_count
def measure_peak_memory(pid: int) -> float:
"""프로세스의 최대 메모리 사용량을 GB 단위로 반환합니다."""
process = psutil.Process(pid)
mem_info = process.memory_info()
return mem_info.rss / (1024 ** 3)다음 표는 Llama 3.2 7B Q4_K_M 모델을 기준으로 주요 하드웨어의 벤치마크 결과를 정리한 것입니다.
| 하드웨어 | 프롬프트 처리 (tok/s) | 생성 (tok/s) | TTFT (ms) | 메모리 (GB) |
|---|---|---|---|---|
| Apple M4 Pro (24GB) | 285 | 38 | 180 | 4.8 |
| Apple M4 Max (48GB) | 420 | 52 | 120 | 4.8 |
| NVIDIA RTX 4090 (24GB) | 580 | 85 | 95 | 5.1 |
| NVIDIA RTX 4070 (12GB) | 310 | 45 | 210 | 5.0 |
| AMD RX 7900 XTX (24GB) | 260 | 35 | 250 | 5.2 |
| Intel Arc A770 (16GB) | 180 | 25 | 350 | 5.3 |
| 하드웨어 | 프롬프트 처리 (tok/s) | 생성 (tok/s) | TTFT (ms) | 전력 (W) |
|---|---|---|---|---|
| Snapdragon 8 Elite (NPU) | 95 | 18 | 520 | 5.2 |
| Apple A18 Pro (ANE) | 110 | 22 | 420 | 4.8 |
| MediaTek Dimensity 9400 | 80 | 15 | 650 | 5.5 |
| Raspberry Pi 5 (CPU) | 12 | 3.5 | 3200 | 8.0 |
위 벤치마크 수치는 동일한 모델과 프롬프트(512 입력 토큰, 128 출력 토큰)로 측정한 참고값입니다. 실제 성능은 운영체제 버전, 드라이버, 메모리 대역폭, 열 관리 상태에 따라 달라질 수 있습니다.
KV 캐시 양자화(KV Cache Quantization)는 어텐션 메커니즘의 Key-Value 캐시를 낮은 정밀도로 저장하여 메모리 사용량을 줄이는 기법입니다. 모델 가중치 양자화와 별개로 적용할 수 있습니다.
# KV 캐시를 Q8_0으로 양자화하여 메모리 절약
./llama-server \
--model models/llama-3.2-7b-q4_k_m.gguf \
--ctx-size 8192 \
--cache-type-k q8_0 \
--cache-type-v q8_0 \
--gpu-layers 99FP16 대비 Q8_0 KV 캐시는 메모리를 약 50% 절약하면서 품질 저하는 거의 없습니다. Q4_0까지 내리면 메모리는 75% 절약되지만, 긴 컨텍스트에서 품질 저하가 눈에 띄기 시작합니다.
컨텍스트 길이가 늘어날수록 메모리와 연산 비용이 급격히 증가합니다. 컨텍스트 프루닝(Context Pruning)은 중요도가 낮은 과거 토큰을 제거하여 컨텍스트를 관리 가능한 크기로 유지하는 기법입니다.
일반적인 전략은 다음과 같습니다.
동일한 시스템 프롬프트를 반복 사용하는 경우, 프롬프트 캐싱(Prompt Caching)을 통해 프롬프트 처리 시간을 대폭 단축할 수 있습니다. llama.cpp의 --slot-save-path 옵션으로 KV 캐시 상태를 디스크에 저장하고 재사용합니다.
# 슬롯 저장 경로를 지정하여 프롬프트 캐싱 활성화
./llama-server \
--model models/llama-3.2-7b-q4_k_m.gguf \
--slot-save-path ./cache/slots \
--ctx-size 4096 \
--gpu-layers 99mmap(Memory-Mapped File)은 모델 파일을 가상 메모리에 매핑하여 실제 물리 메모리 사용을 줄이는 기법입니다. 운영체제가 필요한 페이지만 물리 메모리에 로딩하므로, 모델 크기보다 작은 RAM에서도 추론이 가능합니다.
모델 분할(Model Splitting) 은 하나의 모델을 여러 디바이스나 메모리 영역에 나누어 로딩하는 기법입니다. GPU VRAM이 부족할 때 일부 레이어를 CPU RAM에 배치하는 것이 대표적인 예입니다.
# GPU에 28개 레이어, 나머지는 CPU에서 처리
./llama-server \
--model models/llama-3.2-7b-q4_k_m.gguf \
--gpu-layers 28 \
--threads 8 \
--ctx-size 4096GPU와 CPU 사이에 레이어를 분할하면 데이터 전송 오버헤드가 발생합니다. 가능하면 모든 레이어를 GPU에 올리는 것이 최선이며, 분할이 불가피한 경우에는 어텐션 레이어를 GPU에 우선 배치하는 것이 좋습니다.
모바일 디바이스에서는 성능뿐 아니라 전력 효율도 핵심 지표입니다. 다음 기법들을 통해 전력 소비를 줄일 수 있습니다.
이번 장에서는 온디바이스 AI 시스템의 성능을 정량적으로 측정하는 방법과 병목을 해소하는 최적화 기법을 살펴보았습니다. 핵심 지표를 정의하고, 반복 가능한 벤치마크를 수행하며, 데이터에 기반한 최적화를 진행하는 것이 성공적인 온디바이스 AI 시스템의 기본입니다.
다음 10장에서는 시리즈 전체의 기법을 종합하여, 프라이버시 보존 문서 분석 시스템을 처음부터 끝까지 구축하는 실전 프로젝트를 진행합니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
시리즈 전체의 기법을 종합하여 프라이버시 보존 문서 분석 시스템을 구축합니다. 로컬 LLM, 로컬 임베딩, 로컬 벡터 DB로 완전한 오프라인 AI를 실현합니다.
온디바이스 AI를 활용한 실전 애플리케이션 설계 패턴 — 하이브리드 추론, 오프라인 우선, 프라이버시 보존, 개인화 학습, 그리고 에지-클라우드 협업을 다룹니다.
온디바이스 AI를 위한 하드웨어 가속기 — Apple Neural Engine, Qualcomm NPU, NVIDIA Jetson, Intel NPU의 아키텍처와 성능 특성을 비교합니다.