Інтеграція локальної LLM (Llama.cpp) для офлайн AI-помічника в мобільному додатку
Запуск мовної моделі прямо на смартфоні без інтернету — це вже реальність. Llama.cpp забезпечує працюючий інференс на CPU з опціональним Metal/Vulkan прискоренням. Головне запитання — не "чи можна", а "яку модель та в якому квантуванні вибрати, щоб пристрій не перегрівся через 5 хвилин?"
Моделі та їхні реальні вимоги
Llama.cpp працює з моделями в форматі GGUF. Популярні варіанти для мобільних пристроїв:
| Модель | Квантування | Розмір | RAM | Швидкість (iPhone 14) |
|---|---|---|---|---|
| Llama-3.2-1B | Q4_K_M | 0.8 ГБ | ~1.2 ГБ | 25–35 t/s |
| Llama-3.2-3B | Q4_K_M | 2.0 ГБ | ~2.5 ГБ | 10–15 t/s |
| Phi-3-mini-4k | Q4_K_M | 2.2 ГБ | ~2.8 ГБ | 8–12 t/s |
| Gemma-2-2B | Q4_K_M | 1.6 ГБ | ~2.0 ГБ | 12–18 t/s |
| Qwen2.5-1.5B | Q4_K_M | 1.0 ГБ | ~1.4 ГБ | 20–28 t/s |
На iPhone SE 2-го покоління з 3 ГБ RAM модель Llama-3.2-3B Q4 працює на межі своїх можливостей — OOM можлива при довгих контекстах. Безпечний вибір для широкого набору пристроїв — моделі до 1.5–2 ГБ.
Збирання Llama.cpp для iOS
# Клонування репозиторію
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
# Збирання через CMake для iOS
cmake -B build-ios \
-DCMAKE_TOOLCHAIN_FILE=ios.toolchain.cmake \
-DPLATFORM=OS64 \ # лише arm64
-DLLAMA_METAL=ON \ # Прискорення Metal GPU
-DLLAMA_STATIC=ON
cmake --build build-ios --config Release
Результат — статична бібліотека libllama.a. Створюємо Swift Package з C-bridging header:
// llama_bridge.h
#include "llama.h"
// Обгортки для Swift-сумісного API
void* llama_create_context(const char* model_path, int n_ctx, int n_gpu_layers);
const char* llama_generate_token(void* ctx, const char* prompt);
void llama_free_context(void* ctx);
n_gpu_layers — кількість шарів, загружених на Metal GPU. Значення -1 означає всі шари на GPU. На iPhone 14 з 6 ГБ unified memory встановіть -1. На пристроях з 3 ГБ експериментуйте: занадто багато шарів на GPU спричиняють OOM.
Swift-обгортка для потокового передачі токенів
import Foundation
actor LlamaSession {
private var context: OpaquePointer?
private var model: OpaquePointer?
func load(modelPath: String, contextSize: Int32 = 2048, gpuLayers: Int32 = -1) throws {
var params = llama_model_default_params()
params.n_gpu_layers = gpuLayers
model = llama_load_model_from_file(modelPath, params)
guard model != nil else { throw LlamaError.modelLoadFailed }
var ctxParams = llama_context_default_params()
ctxParams.n_ctx = UInt32(contextSize)
ctxParams.n_batch = 512
context = llama_new_context_with_model(model, ctxParams)
}
func generate(prompt: String) -> AsyncThrowingStream<String, Error> {
AsyncThrowingStream { continuation in
Task.detached(priority: .userInitiated) {
// Токенізація
var tokens = [llama_token](repeating: 0, count: 4096)
let nTokens = llama_tokenize(self.model, prompt, Int32(prompt.utf8.count),
&tokens, 4096, true, false)
// Інференс — по одному токену
for i in 0..<nTokens {
llama_batch_add(&batch, tokens[Int(i)], llama_pos(i), [0], false)
}
while true {
llama_decode(self.context, batch)
let nextToken = llama_sample_token_greedy(self.context, &candidates)
if nextToken == llama_token_eos(self.model) { break }
// Конвертація токена в рядок
var buf = [Int8](repeating: 0, count: 64)
llama_token_to_piece(self.model, nextToken, &buf, 64, 0, true)
let piece = String(cString: buf)
continuation.yield(piece)
}
continuation.finish()
}
}
}
}
Потокова передача токенів через AsyncThrowingStream — користувачі бачать текст по мірі його генерування, не чекаючи повної відповіді. Критично для UX: 10 токенів за секунду сприймається природно, коли текст з'являється поступово.
Android: Llama.cpp через NDK
// CMakeLists.txt в jni/
add_library(llama_jni SHARED llama_jni.cpp)
target_link_libraries(llama_jni llama ggml)
// Kotlin сторона
class LlamaEngine {
init { System.loadLibrary("llama_jni") }
external fun loadModel(modelPath: String, nGpuLayers: Int): Long // повертає handle
external fun generateNext(handle: Long, tokens: IntArray): String
external fun freeModel(handle: Long)
}
На Android — Vulkan backend замість Metal: увімкніть LLAMA_VULKAN=ON в CMakeLists. Підтримується на пристроях з Vulkan 1.1+, по суті на всіх Android 10+.
Проблема Android: процес не має жорсткого обмеження пам'яті як пула — система може вбити додаток (SIGKILL) за нестачі RAM без попередження. ComponentCallbacks2.onTrimMemory(TRIM_MEMORY_RUNNING_CRITICAL) — останній шанс звільнити контекст перед завершенням процесу.
Завантаження моделі: прогрес та верифікація
Файли GGUF важать 1–4 ГБ. Завантажуйте через URLSession (iOS) або WorkManager з DownloadManager (Android):
// iOS: Фоновий URLSession для фонового завантаження
let config = URLSessionConfiguration.background(withIdentifier: "model-download")
config.isDiscretionary = false
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
let task = session.downloadTask(with: modelURL)
task.resume()
// Верифікація SHA256 після завантаження
func verify(fileURL: URL, expectedHash: String) -> Bool {
guard let data = try? Data(contentsOf: fileURL) else { return false }
let hash = SHA256.hash(data: data)
return hash.compactMap { String(format: "%02x", $0) }.joined() == expectedHash
}
Хеші SHA256 моделей публікуються в репозиторіях HuggingFace — перевірте перед завантаженням в пам'ять. Пошкоджена GGUF спричиняє крахи під час парсингу заголовка або пізніше під час інференсу — краще помітити при верифікації.
Теплові обмеження
Llama.cpp на iPhone розігріває пристрій під час довгої генерації. iOS throttling: при перегріванні система знижує тактову частоту, швидкість генерації падає з 25 t/s до 8–10 t/s. Це очікувана поведінка системи.
Практичне рішення: обмежте максимальний контекст (n_ctx) до 1024–2048 для коротких сеансів. Робіть паузи між запитами. Слідкуйте за ProcessInfo.processInfo.thermalState на iOS:
NotificationCenter.default.addObserver(forName: ProcessInfo.thermalStateDidChangeNotification, ...) { _ in
let state = ProcessInfo.processInfo.thermalState
if state == .critical || state == .serious {
// Призупиніть генерацію, сповістіть користувача
}
}
Процес
Вибір моделі для цільових пристроїв → збирання llama.cpp для платформ → Swift/Kotlin обгортка з асинхронною потоковою передачею → завантаження та верифікація → чат-інтерфейс → тепловий стрес-тест.
Орієнтири за часом
Одна платформа, базовий чат-інтерфейс з вибраною моделлю — 3–5 тижнів. Обидві платформи, кілька моделей на вибір, фонове завантаження, управління контекстом — 7–12 тижнів.







