--- license: other pipeline_tag: text-generation language: - ru - en tags: - android - gguf - llama-cpp - on-device - bonsai-27b --- # Bonsai Local for Android **Русский** · [English](README_EN.md) · [Структурированные результаты](BENCHMARKS.md) Исследовательское Android-приложение для полностью локального запуска [Prism ML Bonsai-27B GGUF](https://huggingface.co/prism-ml/Bonsai-27B-gguf) через JNI и специальный форк `llama.cpp`. После помещения GGUF на устройство диалог не использует сервер, облачный API или интернет. ![Bonsai Local с загруженной моделью](screenshots/02-model-loaded.png) ## Что уже работает - полноценная загрузка `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. ## Проверенная конфигурация | Компонент | Значение | |---|---| | ОС хоста | Windows 11 | | CPU хоста | Intel Core Ultra 5 125H | | Android Studio | 2025.2 | | Android Gradle Plugin | 8.13.2 | | Gradle | 8.14.3 | | JDK сборки | JetBrains Runtime 21.0.9 | | compileSdk / targetSdk | 36 / 36 | | minSdk | 30 (Android 11) | | Android NDK | 28.2.13676358 | | CMake | 3.22.1 | | ABI | `arm64-v8a`, `x86_64` | | Тестовый AVD | Android 17 preview, x86_64, 8 ГБ RAM, 16 ГБ data | | PrismML llama.cpp commit | `62061f91088281e65071cc38c5f69ee95c39f14e` | ## Результаты локальных тестов Все числа ниже получены внутри Android-эмулятора, а не настольным `llama-cli`. | Проверка | Результат | |---|---| | Сборка debug APK | успешно | | Установка и запуск Android | успешно | | Распознавание модели | `qwen35 27B Q1_0`, 26.9B params, 3.53 GiB | | Backend | CPU, динамически выбранный x86_64 variant | | Prompt processing, pp64 | **2.89 tok/s** | | Token generation, tg32 | **1.79 tok/s** | | Арифметика `37 × 19` | корректный результат **703** | | Thinking trace | работает | | Русский + Kotlin, лимит 512 токенов | prompt понят, но весь лимит ушёл на thinking; чистый финальный код не выведен | | Полностью offline после импорта | да | | Крэши / FATAL EXCEPTION | не обнаружены | Скриншоты: - [`02-model-loaded.png`](screenshots/02-model-loaded.png) — модель загружена; - [`03-benchmark.png`](screenshots/03-benchmark.png) — benchmark; - [`04-reasoning.png`](screenshots/04-reasoning.png) — реальная генерация и ответ 703; - [`05-russian-kotlin.png`](screenshots/05-russian-kotlin.png) — негативный capability-тест: модель поняла русский Kotlin prompt, но исчерпала лимит в thinking-mode. Скорость эмулятора нельзя переносить на физический телефон: виртуализированный CPU, thermal policy и доступные SIMD-инструкции отличаются. В карточке модели Prism ML публикует существенно более высокие результаты для нативного MLX на современном iPhone; этот проект использует Android/llama.cpp CPU backend. ## Архитектура ```mermaid 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/` — фактические результаты запуска в эмуляторе. ### Поток загрузки 1. `MainActivity` ждёт состояния `InferenceEngine.State.Initialized`. 2. Ищет `.gguf` в `files/models` и `getExternalFilesDir()/models`. 3. Передаёт абсолютный путь в `InferenceEngine.loadModel()`. 4. Kotlin сериализует все вызовы через один `Dispatchers.IO` dispatcher. 5. JNI вызывает `llama_model_load_from_file()`, затем создаёт контекст 8192 токенов, batch 512 и sampler. 6. GGML выбирает подходящую CPU-библиотеку из `nativeLibraryDir`. 7. Chat template формирует сообщения Qwen, а JNI возвращает token pieces как Kotlin `Flow`. Критический фрагмент JNI: ```cpp 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 корутинами. ```kotlin 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 модели: ```text 17EF842E47450CAEB8EAA3EBFBBAB5D2F2278B62B79BE107985FB69A2F819AA0 ``` Официальная карточка указывает около 5.2 ГБ peak memory при 4K context без KV-cache compression. Android `dumpsys meminfo` показывает для процесса меньше, потому что memory-mapped страницы GGUF и page cache учитываются не так, как private native heap. ## Сборка в Android Studio 1. Открыть папку `BonsaiAndroid` в Android Studio. 2. Убедиться, что рядом есть `third_party/llama.cpp`: ```text gpt/ ├── BonsaiAndroid/ └── third_party/llama.cpp/ ``` 3. Установить SDK 36, NDK `28.2.13676358` и CMake `3.22.1`. 4. Проверить путь SDK в `local.properties`. 5. Выполнить `Build > Make Project` или из PowerShell: ```powershell $env:JAVA_HOME = 'C:\Program Files\Android\Android Studio1\jbr' java -classpath gradle\wrapper\gradle-wrapper.jar ` org.gradle.wrapper.GradleWrapperMain :app:assembleDebug ``` Debug APK появляется в: ```text app/build/outputs/apk/debug/app-debug.apk ``` Проверенный итоговый артефакт дополнительно скопирован в: ```text release/BonsaiLocal-debug.apk ``` Его размер — 120 762 843 байта, SHA-256: ```text BDAF2D9EE7EE1BBB2A424242678D75AC35F2B770973E3F4C9E58639CE8F93E5C ``` Первая сборка долгая: генерируются сотни C/C++ объектов и несколько CPU variants для двух ABI. Инкрементальные сборки значительно быстрее. ### Привязка llama.cpp в CMake Проект был вынесен из `examples/llama.android`, поэтому относительный путь исходников пришлось изменить: ```cmake set(LLAMA_SRC ${CMAKE_CURRENT_LIST_DIR}/../../../../../third_party/llama.cpp) add_subdirectory(${LLAMA_SRC} build-llama) ``` Для Android компилируются обе ABI: ```kotlin ndk { abiFilters += listOf("arm64-v8a", "x86_64") } ``` ## Установка модели ### Обычный пользовательский путь 1. Скопировать `Bonsai-27B-Q1_0.gguf` на телефон. 2. Открыть приложение. 3. Нажать **Выбрать GGUF** и выбрать файл. 4. Дождаться копирования и загрузки. ### Воспроизводимый путь для эмулятора После первой установки APK: ```powershell 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. ```powershell $prompt = 'Ответь по-русски одним предложением.' $b64 = [Convert]::ToBase64String( [Text.Encoding]::UTF8.GetBytes($prompt) ) adb shell am start ` -n com.prismml.bonsailocal/.MainActivity ` --es prompt_b64 $b64 ``` В коде extra декодируется без сетевых вызовов: ```kotlin 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 kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } } ``` ### 4. Android logging API `__android_log_is_loggable()` доступен только с API 30, а ранняя конфигурация использовала minSdk 28. Решение: локальный фильтр log level и итоговый minSdk 30. ### 5. Эмулятор был слишком маленьким Существующий Pixel_7 имел 2 ГБ RAM и раздел data 6 ГБ, из которых свободно около 1.1 ГБ. Модель туда не помещалась. Создан отдельный AVD `Bonsai_27B_Test` с 8 ГБ RAM и 16 ГБ data, не затрагивающий пользовательский Pixel_7. ### 6. 16 KB memory pages Android 15+ требует 16 KB-compatible native libraries. Проект использует NDK r28 и AGP 8.13.2. Проверено: - ELF `LOAD` alignment: `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 делает даже короткие тесты долгими Запрос с требованием вернуть одно число всё равно сгенерировал подробный `` блок. Это ожидаемое поведение модели и полезная проверка 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 не подключён. ## Куда развивать исследование 1. Отделить `` в сворачиваемую UI-панель. 2. Добавить resumable WorkManager/foreground download с SHA-256. 3. Подключить `mmproj` и Android Photo Picker для vision. 4. Исследовать Vulkan backend на Android GPU и сравнить CPU/Vulkan. 5. Подключить Q4 KV cache и замерить RAM на 8K/32K/100K. 6. Проверить DSpark drafter, когда Android path будет поддержан форком. 7. Добавить Room для истории диалогов и export/import sessions. 8. Собрать per-ABI APK/AAB, чтобы не поставлять обе native архитектуры каждому устройству. 9. Добавить macrobenchmark: cold load, first-token latency, sustained tok/s, thermal throttling и energy usage. 10. Запустить тот же 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-приложения. | Проект | Что уже делает | Отличие этого исследования | |---|---|---| | [llama.cpp Android](https://github.com/ggml-org/llama.cpp/blob/master/docs/android.md) | официальный JNI/sample и CPU variants | проверена конкретная редкая Q1_0 hybrid-модель и собран готовый APK | | [PocketPal AI](https://github.com/a-ghorbani/pocketpal-ai) | зрелый GGUF-клиент, загрузки HF и benchmark | универсальный продукт, а здесь узкий воспроизводимый Bonsai-27B test case | | [ChatterUI](https://github.com/Vali-98/ChatterUI) | GGUF и API-чаты через React Native | здесь минимальный native Android/JNI путь без React Native | | [MLC LLM](https://llm.mlc.ai/docs/deploy/android.html) | GPU-oriented Android SDK и demo | другой model format/toolchain; текущий проект использует исходный GGUF и CPU | | [MNN](https://github.com/alibaba/MNN/tree/master/apps/Android/MnnLlmChat) | производительный мультимодальный Android-клиент | значительно шире и быстрее; этот репозиторий проще как regression fixture для Q1_0/GDN | | [ExecuTorch](https://docs.pytorch.org/executorch/stable/llm/run-on-android.html) | AAR и experimental LLM Java API | требует export в `.pte`; здесь тестируется GGUF/llama.cpp путь | Поиск по открытым 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-проектов.