livadies/Bonsai-27B-Android-Local
Bonsai Local for Android
Русский · English · Структурированные результаты
Исследовательское Android-приложение для полностью локального запуска Prism ML Bonsai-27B GGUF через JNI и специальный форк llama.cpp. После помещения GGUF на устройство диалог не использует сервер, облачный API или интернет.
Что уже работает
- полноценная загрузка
Bonsai-27B-Q1_0.ggufразмером 3 803 452 480 байт; - inference внутри Android-процесса, без сервера на ПК;
- потоковый чат и thinking-mode;
- автообнаружение GGUF в приватной и app-specific external папках;
- ручной импорт любого совместимого GGUF через Android Storage Access Framework;
- встроенный benchmark prompt processing / token generation;
- APK для
arm64-v8aреального телефона иx86_64эмулятора; - Android adaptive icon и отдельные raster mipmaps;
- debug-вход
prompt_b64для воспроизводимых автоматических тестов Unicode prompts.
Проверенная конфигурация
Результаты локальных тестов
Все числа ниже получены внутри Android-эмулятора, а не настольным llama-cli.
Скриншоты:
- `02-model-loaded.png` — модель загружена;
- `03-benchmark.png` — benchmark;
- `04-reasoning.png` — реальная генерация и ответ 703;
- `05-russian-kotlin.png` — негативный capability-тест: модель поняла русский Kotlin prompt, но исчерпала лимит в thinking-mode.
Скорость эмулятора нельзя переносить на физический телефон: виртуализированный CPU, thermal policy и доступные SIMD-инструкции отличаются. В карточке модели Prism ML публикует существенно более высокие результаты для нативного MLX на современном iPhone; этот проект использует Android/llama.cpp CPU backend.
Архитектура
flowchart TD
UI["Android UI · MainActivity"] --> API["InferenceEngine Kotlin API"]
API --> DISP["Single-thread coroutine dispatcher"]
DISP --> JNI["JNI · libai-chat.so"]
JNI --> COMMON["llama-common · chat template · sampler"]
JNI --> LLAMA["PrismML llama.cpp"]
LLAMA --> GGML["GGML CPU backend loader"]
GGML --> ABI{"Device ABI / CPU features"}
ABI --> ARM["ARM variants · NEON / DOTPROD / I8MM / SVE / SME"]
ABI --> X86["x86_64 variants · SSE4 / AVX2 / AVX512 / AMX"]
GGUF["Bonsai-27B-Q1_0.gguf · 3.80 GB"] --> LLAMAМодули
app/— UI, импорт модели, чат, benchmark и тестовый Base64 intent.lib/— Android-friendly Kotlin API и JNI wrapper.third_party/llama.cpp/— форк PrismML с Q1_0 и hybrid-attention kernels; находится рядом с проектом, на уровень вышеBonsaiAndroid.models/— локальная копия GGUF для исследования; модель не упаковывается в APK.screenshots/— фактические результаты запуска в эмуляторе.
Поток загрузки
MainActivityждёт состоянияInferenceEngine.State.Initialized.- Ищет
.ggufвfiles/modelsиgetExternalFilesDir()/models. - Передаёт абсолютный путь в
InferenceEngine.loadModel(). - Kotlin сериализует все вызовы через один
Dispatchers.IOdispatcher. - JNI вызывает
llama_model_load_from_file(), затем создаёт контекст 8192 токенов, batch 512 и sampler. - GGML выбирает подходящую CPU-библиотеку из
nativeLibraryDir. - Chat template формирует сообщения Qwen, а JNI возвращает token pieces как Kotlin
Flow<String>.
Критический фрагмент JNI:
llama_model_params model_params = llama_model_default_params();
g_model = llama_model_load_from_file(model_path, model_params);
llama_context_params ctx_params = llama_context_default_params();
ctx_params.n_ctx = 8192;
ctx_params.n_batch = 512;
ctx_params.n_threads = n_threads;
g_context = llama_init_from_model(g_model, ctx_params);Сериализация native calls важна: глобальные llama_model, llama_context, llama_batch и sampler не должны одновременно изменяться UI и benchmark корутинами.
private val llamaDispatcher = Dispatchers.IO.limitedParallelism(1)
override suspend fun loadModel(pathToModel: String) =
withContext(llamaDispatcher) {
load(pathToModel)
prepare()
}Что подтверждено в native runtime
При фактической загрузке полного GGUF runtime сообщил:
- 64 transformer blocks;
- Q1_0 binary tensors;
- recurrent state около 149.62 MiB;
- CPU compute buffer около 523.02 MiB;
- Flash Attention включён автоматически;
- fused Gated Delta Net работает в autoregressive и chunked режимах;
- graph содержит около 3703 nodes и один split.
Bonsai-27B не MoE-модель. У неё нет набора экспертов и router, поэтому проверка «всех экспертов» неприменима. Это dense 27B hybrid-attention model: примерно 75% слоёв используют linear/recurrent attention и 25% — full attention.
Текущий APK текстовый. Дополнительные компоненты из репозитория модели не подключены:
Bonsai-27B-mmproj-Q8_0.gguf— vision tower;Bonsai-27B-dspark-Q4_1.gguf— speculative decoding drafter.
Требования к устройству
- Android 11 или новее;
- 64-bit ARM или x86_64;
- минимум 5 ГБ свободного места только под GGUF, комфортно 8+ ГБ;
- рекомендуется 8 ГБ RAM или больше;
- для импорта через picker временно может потребоваться место для копии;
- первый старт модели может занимать десятки секунд.
Вес модели не включён внутрь APK, потому что APK с asset размером 3,8 ГБ непрактичен. Проверенная копия опубликована рядом с проектом как models/Bonsai-27B-Q1_0.gguf, поэтому её можно скачать из этого репозитория без отдельного поиска. SHA-256 модели:
17EF842E47450CAEB8EAA3EBFBBAB5D2F2278B62B79BE107985FB69A2F819AA0Официальная карточка указывает около 5.2 ГБ peak memory при 4K context без KV-cache compression. Android dumpsys meminfo показывает для процесса меньше, потому что memory-mapped страницы GGUF и page cache учитываются не так, как private native heap.
Сборка в Android Studio
- Открыть папку
BonsaiAndroidв Android Studio. - Убедиться, что рядом есть
third_party/llama.cpp:
gpt/
├── BonsaiAndroid/
└── third_party/llama.cpp/- Установить SDK 36, NDK
28.2.13676358и CMake3.22.1. - Проверить путь SDK в
local.properties. - Выполнить
Build > Make Projectили из PowerShell:
$env:JAVA_HOME = 'C:\Program Files\Android\Android Studio1\jbr'
java -classpath gradle\wrapper\gradle-wrapper.jar `
org.gradle.wrapper.GradleWrapperMain :app:assembleDebugDebug APK появляется в:
app/build/outputs/apk/debug/app-debug.apkПроверенный итоговый артефакт дополнительно скопирован в:
release/BonsaiLocal-debug.apkЕго размер — 120 762 843 байта, SHA-256:
BDAF2D9EE7EE1BBB2A424242678D75AC35F2B770973E3F4C9E58639CE8F93E5CПервая сборка долгая: генерируются сотни C/C++ объектов и несколько CPU variants для двух ABI. Инкрементальные сборки значительно быстрее.
Привязка llama.cpp в CMake
Проект был вынесен из examples/llama.android, поэтому относительный путь исходников пришлось изменить:
set(LLAMA_SRC ${CMAKE_CURRENT_LIST_DIR}/../../../../../third_party/llama.cpp)
add_subdirectory(${LLAMA_SRC} build-llama)Для Android компилируются обе ABI:
ndk {
abiFilters += listOf("arm64-v8a", "x86_64")
}Установка модели
Обычный пользовательский путь
- Скопировать
Bonsai-27B-Q1_0.ggufна телефон. - Открыть приложение.
- Нажать Выбрать GGUF и выбрать файл.
- Дождаться копирования и загрузки.
Воспроизводимый путь для эмулятора
После первой установки APK:
adb -s emulator-5556 shell mkdir -p `
/sdcard/Android/data/com.prismml.bonsailocal/files/models
adb -s emulator-5556 push .\models\Bonsai-27B-Q1_0.gguf `
/sdcard/Android/data/com.prismml.bonsailocal/files/models/После перезапуска приложение найдёт модель автоматически.
Воспроизводимые тесты Unicode
Debug Activity принимает Base64 UTF-8 prompt. Это обход нестабильного Unicode input у ADB-клавиатуры preview-эмулятора и не участвует в обычном UI.
$prompt = 'Ответь по-русски одним предложением.'
$b64 = [Convert]::ToBase64String(
[Text.Encoding]::UTF8.GetBytes($prompt)
)
adb shell am start `
-n com.prismml.bonsailocal/.MainActivity `
--es prompt_b64 $b64В коде extra декодируется без сетевых вызовов:
String(Base64.decode(encoded, Base64.NO_WRAP), Charsets.UTF_8)Проблемы, встреченные при разработке
1. Готового Android runtime у коллекции нет
Коллекция публикует GGUF и специальный PrismML fork, но не готовый AAR/APK. Решение: официальный examples/llama.android из форка использован как база, а CMake собирает runtime прямо внутри Gradle.
2. Стандартный llama.cpp недостаточен
Модель использует Q1_0 и Qwen 3.5/3.6 hybrid architecture. Решение: закреплён именно PrismML fork commit 62061f9, содержащий нужные quantization и Gated Delta Net paths.
3. JDK 17 toolchain отсутствовал
Gradle запускался на JBR 21, а исходный sample требовал установленный JDK 17 через jvmToolchain(17). Gradle не имел repository для автоматической загрузки. Решение: убрать принудительный поиск отдельного toolchain и явно компилировать Java/Kotlin bytecode target 17:
kotlin {
compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
}4. Android logging API
__android_log_is_loggable() доступен только с API 30, а ранняя конфигурация использовала minSdk 28. Решение: локальный фильтр log level и итоговый minSdk 30.
5. Эмулятор был слишком маленьким
Существующий Pixel7 имел 2 ГБ RAM и раздел data 6 ГБ, из которых свободно около 1.1 ГБ. Модель туда не помещалась. Создан отдельный AVD `Bonsai27BTest` с 8 ГБ RAM и 16 ГБ data, не затрагивающий пользовательский Pixel7.
6. 16 KB memory pages
Android 15+ требует 16 KB-compatible native libraries. Проект использует NDK r28 и AGP 8.13.2. Проверено:
- ELF
LOADalignment:2**14для native.so; zipalign -v -c -P 16 4 app-debug.apk:Verification successful.
Android 17 preview system image дополнительно показывает экспериментальный RELRO compatibility dialog для части динамически загружаемых CPU variants. Неиспользуемый androidx.datastore был удалён из общего dependency bundle, что исключило libdatastore_shared_counter.so из APK. После этого в APK осталось 32 целевых native-библиотеки вместо 36. APK продолжает работать в page-size compatibility mode. Для production перед публикацией нужно повторить проверку на стабильном Android 15/16 16 KB image и обновить NDK/PrismML fork, если preview-проверка станет обязательной.
7. Большой GGUF нельзя дублировать бездумно
Импорт в private storage может временно требовать две копии. Для тестового AVD модель отправлялась сразу в app-specific external directory, которую приложение сканирует при старте.
8. Thinking mode делает даже короткие тесты долгими
Запрос с требованием вернуть одно число всё равно сгенерировал подробный <think> блок. Это ожидаемое поведение модели и полезная проверка reasoning, но на CPU-эмуляторе занимает минуты. UI генерирует асинхронно и не блокирует main thread. Более сложный русский Kotlin prompt подтвердил понимание задачи, но за 512 токенов модель не вышла из thinking в финальный ответ. Это не падение runtime, а ограничение текущей политики генерации; для прикладного UI нужны отдельная панель reasoning, больший лимит или поддерживаемое моделью отключение thinking-mode.
Ограничения текущей версии
- только text-to-text; vision projection не загружается;
- только CPU backend в протестированном AVD;
- нет встроенного resumable downloader и проверки SHA-256 в UI;
- модель опубликована отдельно от APK в папке
models/; - история чата живёт в памяти Activity и не сохраняется после полного restart;
- контекст приложения ограничен 8192 токенами, хотя модель обучена на гораздо более длинный контекст;
- thinking-теги показываются как обычный текст;
- на устройствах с 6 ГБ RAM возможен LMK/OOM;
- DSpark speculative decoding не подключён.
Куда развивать исследование
- Отделить
<think>в сворачиваемую UI-панель. - Добавить resumable WorkManager/foreground download с SHA-256.
- Подключить
mmprojи Android Photo Picker для vision. - Исследовать Vulkan backend на Android GPU и сравнить CPU/Vulkan.
- Подключить Q4 KV cache и замерить RAM на 8K/32K/100K.
- Проверить DSpark drafter, когда Android path будет поддержан форком.
- Добавить Room для истории диалогов и export/import sessions.
- Собрать per-ABI APK/AAB, чтобы не поставлять обе native архитектуры каждому устройству.
- Добавить macrobenchmark: cold load, first-token latency, sustained tok/s, thermal throttling и energy usage.
- Запустить тот же test suite на физическом Snapdragon/Dimensity и сравнить NEON, DOTPROD, I8MM и SVE variants.
Ценность для сообщества и похожие проекты
Локальные LLM на Android уже не являются новой идеей. Официальный llama.cpp содержит Android Studio sample и динамический выбор CPU kernels; PocketPal AI и ChatterUI запускают разные GGUF на телефоне; MLC LLM, MNN и ExecuTorch предлагают собственные Android runtime и demo-приложения.
Поиск по открытым Hugging Face и GitHub проектам на дату публикации не выявил другой воспроизводимой сборки именно Bonsai-27B Q1_0 внутри Android APK. Это не доказательство абсолютного первенства, но практическая новизна публикации состоит в сочетании следующих элементов:
- полный проверенный GGUF, APK, исходники и checksum находятся вместе;
- зафиксированы реальные Android pp/tg замеры, runtime paths и screenshots;
- описаны 16 KB page-size и RELRO проблемы preview-Android;
- опубликован не только успешный ответ 703, но и отрицательный Kotlin-тест;
- документация дана на русском и английском языках.
Сейчас это полезный engineering baseline и regression artifact, а не новая научная архитектура или production-конкурент PocketPal/MNN. Наибольшую ценность следующий этап даст после ARM64-тестов на физических Snapdragon/Dimensity, измерений RAM/энергии/first-token latency, CI-сборки и upstream PR с найденными Android-исправлениями.
Лицензии
- Bonsai-27B GGUF: Apache-2.0 согласно карточке модели.
- llama.cpp / PrismML fork: см. лицензии в
third_party/llama.cpp. - При распространении APK и модели необходимо сохранить соответствующие
LICENSEиNOTICEфайлов upstream-проектов.
