Flutter 앱에서 온디바이스 LLM(Gemma/Phi-3) 실행하기: MediaPipe LLM Inference API 활용법

최근 모바일 애플리케이션 개발 분야의 화두는 단연 **온디바이스 AI(On-device AI)**입니다. 클라우드 기반 LLM API(OpenAI GPT-4o, Anthropic Claude)를 호출하는 방식은 높은 품질을 제공하지만, API 호출 요금 폭증, 네트워크 지연 시간(Latency), 및 개인정보(Privacy) 유출 우려라는 한계를 갖고 있습니다.
이러한 한계를 돌파하기 위해 구글이 출시한 MediaPipe LLM Inference API는 온디바이스 환경에서 Gemma-2B, Phi-3-Mini, Falcon-RW-1B와 같은 경량화 언어 모델을 스마트폰 GPU 및 NPU 자원을 활용해 직접 추론(Inference)할 수 있게 해줍니다.
이 글에서는 Flutter 앱에 MediaPipe LLM Inference API 패키지를 세팅하고, 모델 파일양자화(Quantization - int4/int8), 스트리밍 텍스트 생성(Streaming Token Response), 그리고 모바일 메모리 OOM(Out Of Memory) 방지 최적화 기법을 완벽히 다룹니다.
핵심 요약
- 온디바이스 LLM의 장점: 네트워크 접속이 없는 오프라인 상태에서도 동작하며, 서버 비용 $0, 응답 지연 시간 최적화, 완벽한 사용자 개인정보 보호를 보장합니다.
- MediaPipe LLM Inference API: Cross-platform(Android/iOS)을 지원하는 구글의 온디바이스 런타임 엔진으로, GPU 가속을 통해 모바일 기기에서 초당 15~25 토큰 생성 속도를 출력합니다.
- 모델 양자화(Quantization): 4-bit(int4) 양자화된
.bin/.task모델 파일을 사용하여 Gemma-2B 모델 기준 메모리 점유율을 1.5GB 미만으로 축소합니다.- Flutter Native Binding: FFI 또는 MethodChannel을 통해 C++/Kotlin/Swift 파이프라인과 연결하여 텍스트 스트림을 Flutter
StreamBuilder에 직접 바인딩합니다.
1. On-Device LLM vs Cloud LLM 아키텍처 비교
| 항목 | Cloud LLM (OpenAI / Claude API) | On-Device LLM (MediaPipe + Gemma) |
|---|---|---|
| 운영 비용 (Cost) | API 호출량 따라 지속 증가 ($$$) | 서버 비용 $0 (모바일 자원 활용) |
| 네트워크 의존성 | 필수 (오프라인 시 사용 불가) | 100% 오프라인 동작 가능 |
| 개인정보 보호 (Privacy) | 사용자 데이터가 외부 서버로 전송됨 | 기기 내부에서만 처리 (외부 유출 Zero) |
| 응답 Latency | 네트워크 RTT 포함 1.5s ~ 4s | 첫 토큰 200ms 미만 (스트리밍) |
| 앱 번들 / 메모리 | 번들 추가 비용 없음 (~10MB) | 모델 파일 저장 공간 1.2GB~2.0GB 필요 |
2. MediaPipe LLM Inference 엔진 설치 및 모델 배치
Android와 iOS 기기에서 4-bit 양자화된 Gemma-2B (gemma-2b-it-gpu-int4.bin) 모델 파일을 다운로드하여 앱의 로컬 스토리지에 배치해야 합니다.
2.1 Flutter 패키지 설정 및 Native 바인딩 (pubspec.yaml)
dependencies:
flutter:
sdk: flutter
path_provider: ^2.1.4
flutter_highlighter: ^0.0.3
# ios/Podfile 및 android/app/build.gradle 에 MediaPipe tasks-genai 라이브러리 추가
3. Flutter에서 MediaPipe LLM Inference 호출 코드
MediaPipe C++ / Native API를 Wrap하는 Service 클래스를 작성합니다.
// lib/services/ondevice_llm_service.dart
import 'dart:async';
import 'package:flutter/services.dart';
class OnDeviceLlmService {
static const MethodChannel _channel = MethodChannel('dev.effidev.mediapipe_llm');
static const EventChannel _streamChannel = EventChannel('dev.effidev.mediapipe_llm/stream');
bool _isInitialized = false;
/// 온디바이스 LLM 모델 초기화 (Gemma 2B int4)
Future<void> initializeModel(String modelPath) async {
try {
await _channel.invokeMethod('initModel', {
'modelPath': modelPath,
'maxTokens': 512,
'topK': 40,
'temperature': 0.7,
});
_isInitialized = true;
} on PlatformException catch (e) {
throw Exception("LLM 초기화 실패: ${e.message}");
}
}
/// 프롬프트 입력 및 스트리밍 토큰 수신
Stream<String> generateResponseStream(String prompt) {
if (!_isInitialized) {
throw StateError("모델이 초기화되지 않았습니다.");
}
// Native 영역에서 토큰 생성시마다 Stream 전달
_channel.invokeMethod('generateResponse', {'prompt': prompt});
return _streamChannel
.receiveBroadcastStream()
.map((event) => event.toString());
}
/// 메모리 해제
Future<void> dispose() async {
await _channel.invokeMethod('releaseModel');
_isInitialized = false;
}
}
4. UI 구현: StreamBuilder 기반 스트리밍 챗 인터페이스
// lib/views/chat_page.dart
import 'package:flutter/material.dart';
import '../services/ondevice_llm_service.dart';
class OnDeviceChatPage extends StatefulWidget {
const OnDeviceChatPage({super.key});
@override
State<OnDeviceChatPage> createState() => _OnDeviceChatPageState();
}
class _OnDeviceChatPageState extends State<OnDeviceChatPage> {
final _llmService = OnDeviceLlmService();
final _textController = TextEditingController();
String _currentResponse = "";
bool _isLoading = false;
void _sendMessage() {
final prompt = _textController.text.trim();
if (prompt.isEmpty) return;
setState(() {
_currentResponse = "";
_isLoading = true;
_textController.clear();
});
_llmService.generateResponseStream(prompt).listen(
(token) {
setState(() {
_currentResponse += token;
_isLoading = false;
});
},
onError: (err) {
setState(() {
_currentResponse = "에러 발생: $err";
_isLoading = false;
});
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("MediaPipe Gemma On-Device AI")),
body: Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Text(
_currentResponse.isEmpty && _isLoading
? "생각 중..."
: _currentResponse,
style: const TextStyle(fontSize: 16),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _textController,
decoration: const InputDecoration(
hintText: "온디바이스 AI에게 질문하세요...",
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.send),
onPressed: _sendMessage,
),
],
),
),
],
),
);
}
}
5. 모바일 메모리(OOM) 방지 및 GPU 가속 설정 팁
- Int4 양자화 모델 필수 사용: 16-bit float 모델은 4GB 이상의 VRAM을 요구하여 모바일 기기에서 앱이 강제 종료(OOM)됩니다. 반드신 MediaPipe 지원 4-bit int4 양자화
.task모델을 사용하세요. - GPU Delegate 튜닝: iOS Metal 및 Android Vulkan GPU delegate를 활성화하여 CPU 추론 대비 속도를 4배 이상 향상시킵니다.
- Background Lifecycle 해제: 앱이 백그라운드로 전환될 때 모델 메모리를 즉시 해제하거나 swap 처리하여 OS에 의한 앱 킬(App Kill) 현상을 방지합니다.
MediaPipe LLM Inference API를 활용하면 모바일 Flutter 앱 개발자는 백엔드 서버 인프라 부담 없이 강력한 AI 챗봇 및 자동 완성 기능을 기기 내부에서 오프라인으로 구현할 수 있습니다.