livadies's picture
Document community value and compare related Android LLM projects / Анализ пользы и похожих проектов
bc577b4 verified
|
Raw
History Blame Contribute Delete
23.2 kB
---
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<String>`.
Критический фрагмент 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 делает даже короткие тесты долгими
Запрос с требованием вернуть одно число всё равно сгенерировал подробный
`<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 не подключён.
## Куда развивать исследование
1. Отделить `<think>` в сворачиваемую 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-проектов.