Интеграция On-Device LLM (MLC LLM) для офлайн AI-ассистента в мобильном приложении
MLC LLM (Machine Learning Compilation LLM) — проект от команды TVM, который компилирует языковые модели непосредственно под конкретный железный таргет. В отличие от llama.cpp, работающего через универсальный C++ backend, MLC генерирует оптимизированный Metal код для iPhone или Vulkan для Android в момент компиляции модели. Это даёт ощутимый прирост скорости — особенно на Apple Silicon.
Чем MLC отличается от llama.cpp
Llama.cpp — интерпретирует GGUF граф в рантайме, использует Metal через общий путь. MLC LLM — AOT (Ahead-Of-Time) компиляция: Python-скрипт генерирует .metal/.vulkan шейдеры специфично для данной модели и данного устройства. Ценой большего времени подготовки получаем более эффективные шейдеры.
На iPhone 14 Pro с Llama-3.2-3B Q4: llama.cpp — 10–14 t/s, MLC LLM — 16–22 t/s. Разница заметна.
Компиляция модели под iOS
# Установка mlc-llm
pip install mlc-llm
# Компиляция модели под iPhone (Metal)
mlc_llm convert_weight \
./Llama-3.2-3B-Instruct/ \
--quantization q4f16_1 \
--output mlc-llm-weights/
mlc_llm gen_config \
./Llama-3.2-3B-Instruct/ \
--quantization q4f16_1 \
--conv-template llama-3 \
--output mlc-llm-config/
mlc_llm compile \
mlc-llm-config/mlc-chat-config.json \
--device iphone \
--output dist/libs/Llama-3.2-3B-Instruct-q4f16_1-iphone.tar
Результат — архив с .dylib и Metal шейдерами. Встраивается в Xcode проект.
Для Android аналогично с --device android:
mlc_llm compile \
mlc-llm-config/mlc-chat-config.json \
--device android \
--output dist/libs/Llama-3.2-3B-Instruct-q4f16_1-android.tar
iOS SDK: Swift интеграция
MLC LLM предоставляет официальный Swift Package — mlc-swift:
import MLCSwift
// Инициализация движка
let engine = MLCEngine()
// Загрузка модели (асинхронно)
try await engine.reload(
modelPath: Bundle.main.path(forResource: "Llama-3.2-3B", ofType: nil)!,
modelLib: "Llama-3.2-3B-Instruct-q4f16_1-iphone" // имя .dylib без расширения
)
// Стриминг через async/await
let messages: [ChatCompletionMessage] = [
.init(role: .system, content: "You are a helpful assistant."),
.init(role: .user, content: "Объясни что такое RAG в машинном обучении")
]
let request = ChatCompletionRequest(messages: messages, stream: true)
for await chunk in try await engine.chat.completions.create(request) {
if let delta = chunk.choices.first?.delta.content {
// Добавляем дельту к UI в реальном времени
await MainActor.run { self.responseText += delta }
}
}
API максимально приближен к OpenAI Chat Completions API — это упрощает переиспользование кода между серверным и on-device вариантом.
Android SDK: Kotlin интеграция
import ai.mlc.mlcllm.MLCEngine
class LLMViewModel(application: Application) : AndroidViewModel(application) {
private val engine = MLCEngine()
suspend fun loadModel(modelPath: String, modelLib: String) {
engine.reload(modelPath, modelLib)
}
fun chat(userMessage: String): Flow<String> = flow {
val messages = listOf(
ChatCompletionMessage(role = MessageRole.user, content = userMessage)
)
val request = ChatCompletionRequest(messages = messages, stream = true)
engine.chat.completions.create(request).collect { chunk ->
chunk.choices.firstOrNull()?.delta?.content?.let { delta ->
emit(delta)
}
}
}.flowOn(Dispatchers.IO)
}
flowOn(Dispatchers.IO) — инференс не должен блокировать main thread. UI подписывается на Flow через collectAsState() в Compose или launchWhenResumed во Fragment.
Управление памятью моделей
Одна модель в памяти одновременно — правило для мобиля. Выгрузка:
await engine.unload()
// Явная выгрузка освобождает Metal bufers и GPU memory
// После этого можно загрузить другую модель
На iOS Metal память — отдельный пул от system RAM, но общий с другими приложениями. Если пользователь переключится на тяжёлое приложение (игра, камера), система может принудительно вытеснить Metal ресурсы — модель нужно перегружать.
// Обработка вытеснения Metal ресурсов
NotificationCenter.default.addObserver(
forName: .MLCEngineModelUnloaded, // или собственный механизм детекции
object: nil, queue: .main
) { [weak self] _ in
Task { try await self?.engine.reload(...) }
}
Скачивание и управление моделями
Веса модели не встраиваются в app bundle (ограничение App Store — 4 ГБ на весь пакет, а веса могут быть 2–4 ГБ). Скачиваем при первом запуске или по запросу:
// Background download через URLSession
func downloadModel(from url: URL, modelName: String) async throws {
let destinationURL = Self.modelsDirectory.appendingPathComponent(modelName)
guard !FileManager.default.fileExists(atPath: destinationURL.path) else { return }
let (tempURL, _) = try await URLSession.shared.download(from: url)
try FileManager.default.moveItem(at: tempURL, to: destinationURL)
}
static var modelsDirectory: URL {
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("MLCModels")
}
applicationSupportDirectory — правильное место для больших данных приложения (не Documents, который пользователь видит в Files.app).
Когда MLC, когда llama.cpp
MLC LLM предпочтительнее когда: важна максимальная скорость на конкретном устройстве, целевые устройства хорошо известны (можно компилировать под конкретные архитектуры), используете официальные модели с HuggingFace (Llama, Phi, Gemma, Mistral).
llama.cpp предпочтительнее когда: нужна гибкость в выборе квантований, модель приходит в GGUF от партнёров, важна поддержка старых устройств, нужен кастомный семплинг (beam search, специфические параметры температуры).
Процесс
Выбор модели под задачу и парк устройств → компиляция под iOS/Android таргеты → интеграция SDK → реализация UI чата с async стримингом → пайплайн скачивания весов → тестирование на тепловые ограничения.
Ориентиры по срокам
Одна платформа, одна модель, базовый чат — 3–5 недель. Обе платформы, несколько моделей с переключением, система управления весами — 7–11 недель.







