Bonsai Local for Android

Русский · English · Структурированные результаты

Исследовательское Android-приложение для полностью локального запуска Prism ML Bonsai-27B GGUF через JNI и специальный форк llama.cpp. После помещения GGUF на устройство диалог не использует сервер, облачный API или интернет.

Bonsai Local с загруженной моделью

Что уже работает

  • полноценная загрузка 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 не обнаружены

Скриншоты:

Скорость эмулятора нельзя переносить на физический телефон: виртуализированный 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/ — фактические результаты запуска в эмуляторе.

Поток загрузки

  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:

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

  1. Открыть папку BonsaiAndroid в Android Studio.

  2. Убедиться, что рядом есть third_party/llama.cpp:

    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:

    $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 появляется в:

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")
}

Установка модели

Обычный пользовательский путь

  1. Скопировать Bonsai-27B-Q1_0.gguf на телефон.
  2. Открыть приложение.
  3. Нажать Выбрать GGUF и выбрать файл.
  4. Дождаться копирования и загрузки.

Воспроизводимый путь для эмулятора

После первой установки 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. Эмулятор был слишком маленьким

Существующий 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 официальный JNI/sample и CPU variants проверена конкретная редкая Q1_0 hybrid-модель и собран готовый APK
PocketPal AI зрелый GGUF-клиент, загрузки HF и benchmark универсальный продукт, а здесь узкий воспроизводимый Bonsai-27B test case
ChatterUI GGUF и API-чаты через React Native здесь минимальный native Android/JNI путь без React Native
MLC LLM GPU-oriented Android SDK и demo другой model format/toolchain; текущий проект использует исходный GGUF и CPU
MNN производительный мультимодальный Android-клиент значительно шире и быстрее; этот репозиторий проще как regression fixture для Q1_0/GDN
ExecuTorch 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-проектов.
Downloads last month
213
GGUF
Model size
27B params
Architecture
qwen35
Hardware compatibility
Log In to add your hardware

1-bit

Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support