chore: declare the app a game, pin what CI claims, and list every flag (#148)

Four things an audit found, none of which changes engine behaviour.

The demo app declares android:appCategory="game". Vendor performance layers
read that attribute to pick a governor profile, and on the OxygenOS test device
it moved the app onto the boosted path: the foreground CPU ceiling went from
1.9/1.65 GHz to the hardware maximum of 3.32/3.80 GHz, measured before and
after. Decode is the most CPU-hungry thing a phone does outside a game. The
effect belongs to the vendor rather than to Android, and Samsung's game service
has historically throttled what it classifies this way, so the manifest, the
changelog and the app README all say to treat a per-device figure as a
measurement. In-app numbers from before this are not comparable with numbers
from after it.

CI now enforces the versions it claims. The format job installs clang-format-18
by name instead of whatever the runner image ships, which happened to be 18 and
would have started failing every PR against an unannounced version on the next
image bump. The APK job builds with NDK r27c, the release that produces
published APKs, so CI stops validating a build nobody installs. It also passes
-DGGML_OPENCL=OFF, the flag whose absence once shipped a stray backend into two
releases. checkout moves to v5, since v4 pins a deprecated Node runtime.

--help lists every one of the fifty flags the CLI accepts. Six were missing.
--io-trace is the one that mattered: a fully documented, guarded diagnostic
that the usage text never mentioned, so the only way to find it was to read
docs/telemetry.md.

.gitignore covers .claude/, which until now was excluded only by a
machine-local ignore file. A clone elsewhere would have shown a second checkout
with build output and .so binaries as untracked, which is precisely the
situation the never-"git add -A" rule exists to survive.
This commit is contained in:
Helldez 2026-08-02 00:44:50 +02:00 • committed by GitHub
parent 4645dc6b09
commit 50e7571bb7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 70 additions and 10 deletions

View file

@ -30,7 +30,7 @@ jobs:
outputs:
code: ${{ steps.decide.outputs.code }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
- id: decide
@ -65,19 +65,22 @@ jobs:
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: clang-format (our sources only)
# Pinned to 18 because AGENTS.md tells contributors to match that version locally. Plain
# `clang-format` is whatever the runner image ships, which happens to be 18 today: an image
# bump would silently start failing every PR against a version nobody was told about.
run: |
sudo apt-get update && sudo apt-get install -y clang-format
sudo apt-get update && sudo apt-get install -y clang-format-18
find core cli tests -type f \( -name '*.cpp' -o -name '*.h' \) \
-print0 | xargs -0 clang-format --dry-run --Werror
-print0 | xargs -0 clang-format-18 --dry-run --Werror
host-linux:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Install deps
@ -96,7 +99,7 @@ jobs:
if: github.event_name != 'push' || startsWith(github.ref, 'refs/tags/v')
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Configure & build (compile-check the Win32 I/O paths)
@ -116,7 +119,7 @@ jobs:
permissions:
contents: write # only used by the tag-triggered release attach step
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Set up JDK 17
@ -128,7 +131,9 @@ jobs:
uses: nttld/setup-ndk@v1
id: ndk
with:
ndk-version: r26d
# r27c == 27.2.12479018, the NDK release-apk.yml builds published APKs with. These two
# must not drift, or CI validates a build nobody installs.
ndk-version: r27c
- name: Cross-compile bmoe-cli (arm64)
env:
ANDROID_NDK_HOME: ${{ steps.ndk.outputs.ndk-path }}
@ -137,7 +142,7 @@ jobs:
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-29 \
-DCMAKE_BUILD_TYPE=Release -DBMOE_BUILD_TESTS=OFF \
-DGGML_NATIVE=OFF -DGGML_OPENMP=OFF \
-DGGML_NATIVE=OFF -DGGML_OPENMP=OFF -DGGML_OPENCL=OFF \
-DGGML_CPU_ARM_ARCH="armv8.2-a+dotprod+fp16" \
-DLLAMA_CURL=OFF
cmake --build build-android -j

View file

@ -42,7 +42,7 @@ jobs:
echo "tag=${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
fi
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
ref: ${{ steps.tag.outputs.tag }}
submodules: recursive

6
.gitignore vendored
View file

@ -52,3 +52,9 @@ __pycache__/
*.jks
*.keystore
keystore.properties
# Agent session state and its worktrees. Ignored belt-and-braces rather than relying on a
# machine-local exclude: a clone elsewhere would otherwise show a second checkout, build output
# and .so binaries as untracked, which is exactly what the never-'git add -A' rule exists to
# survive.
.claude/

View file

@ -116,6 +116,25 @@ Semantic Versioning.
batch. Default `0` (never stop), which is what the host numbers were measured at.
### Changed
- **The demo app declares `android:appCategory="game"`.** Vendor performance layers read that
attribute, and on the OxygenOS test device it moves the app onto the boosted path: with the app
in the foreground the CPU ceiling went from 1.9/1.65 GHz to the hardware maximum of 3.32/3.80 GHz,
measured before and after. Decode is the most CPU-hungry thing a phone does outside a game, so
that is the right path to be on. It cannot be a runtime setting — a manifest attribute is fixed
at install — and the effect belongs to the vendor rather than to Android: neutral on stock
builds, and Samsung's game service has historically throttled what it classifies this way, so any
per-device figure is a measurement rather than a rule. **In-app numbers taken before this change
are not comparable with numbers taken after it.**
- **CI enforces the versions it claims to.** The format job installs `clang-format-18` explicitly
instead of whatever the runner image ships, which happened to be 18 and would have started
failing every PR against an unannounced version on the next image bump; the APK job builds with
NDK r27c, the same release that produces published APKs, so CI stops validating a build nobody
installs; and it passes `-DGGML_OPENCL=OFF`, the flag whose absence once shipped a stray backend
into two releases. `actions/checkout` moves to v5 (v4 pins a deprecated Node runtime).
- **`--help` lists every flag the CLI accepts.** `--io-trace` in particular was a fully documented,
guarded diagnostic that the usage text never mentioned; `--version`, `-h`, `--prefetch-sync` and
the two deprecated `--dense-weights` aliases are named now too. A flag that works but is not
listed reads as an accident rather than a decision.
- **The speculation config generalised to a draft *source*, and the flags with it.** `MtpConfig`
became `SpecConfig` with `DraftSource { none, mtp, ngram }`, and `--mtp-draft N` became `--draft N`
because the width belongs to the verify batch, not to whoever filled it; `--mtp` and `--ngram`

View file

@ -378,8 +378,13 @@ static void print_usage(const char * argv0) {
" same trace at layer granularity: one barrier per layer, so\n"
" coalescing and the expert prefetch survive and the numbers stay\n"
" close to an untraced run. Rows aggregate per layer (op LAYER)\n"
" --io-trace PATH diagnostics: one row per expert read — its (layer, expert,\n"
" projection), size and latency. Needs --moe-stream; this is how a\n"
" flash-bandwidth claim is checked against the reads that made it\n"
" --n-expert-used N override active MoE experts per token (top-k); lower = faster\n"
" but changes the output (quality). 0 = model default\n"
" -h, --help show this text and exit\n"
" --version print the engine version and exit\n"
"\n"
" Sampling (default: greedy/argmax, deterministic):\n"
" --temp F sampling temperature; <= 0 keeps greedy (default 0). > 0 enables\n"
@ -423,6 +428,8 @@ static void print_usage(const char * argv0) {
" ahwb = as anon, but into dma-buf memory the kernel may not reclaim\n"
" at all — not even to zram, which is what anon still pays for.\n"
" Android-only; measured +17.9%% on a long generation, off by default)\n"
" Deprecated aliases kept for old scripts: --dense-odirect means\n"
" `--dense-weights anon`, --no-warm-dense means `--dense-weights mmap`\n"
" --load-all debug: read ALL experts each token (A/B baseline)\n"
" --force-cache allow a cache-mb in the pathological band\n"
" --overlap overlap async expert reads with FFN compute (needs the fork)\n"
@ -430,6 +437,9 @@ static void print_usage(const char * argv0) {
" rest, so the lanes start sooner (needs --overlap and the cache;\n"
" experimental, off by default pending the on-device A/B)\n"
" --prefetch K temporally prefetch the next K layers' experts (needs the cache)\n"
" --prefetch-sync debug/tests only: complete each speculative read on the eval\n"
" thread before returning. Defeats the point (nothing overlaps) but\n"
" makes the integrate-then-hit path deterministic for the gates\n"
" --drop-cold-experts F skip a routed expert that is a cache MISS and carries less than\n"
" F x (1/top-k) of the routing's weight. F in (0, 1]; 1.0 is the\n"
" uniform share and the useful maximum. LOSSY and cache-dependent:\n"

View file

@ -44,6 +44,16 @@ Two build flavors differ only in how a model reaches the device:
- **play** — Play-Store-compliant. No broad storage permission: models come only through the
in-app downloader or the file picker. `./gradlew assemblePlayDebug`.
Both declare `android:appCategory="game"`. That is a performance decision rather than a claim
about what the app is: vendor layers read the attribute to pick a CPU governor profile, and on the
OxygenOS test device it lifted the foreground ceiling from 1.9/1.65 GHz to the hardware maximum of
3.32/3.80 GHz. Decode is the most CPU-hungry thing a phone does outside a game. The effect is the
vendor's, not Android's — neutral on stock builds, and Samsung's game service has historically
throttled apps it classifies this way — so treat any figure as a per-device measurement. It cannot
be toggled at runtime; a manifest attribute is fixed at install, and the only lever would be a
per-flavor manifest. **Numbers measured in the app before this landed are not comparable with
numbers measured after it.**
## Getting a model onto the device
The picker lists every MoE `.gguf` it finds (dense models are filtered out by a gguf-header

View file

@ -15,8 +15,18 @@
filesystem where O_DIRECT works). The dev flavor overlay (src/dev/AndroidManifest.xml)
adds all-files access to also read adb-pushed models in place. -->
<!-- appCategory="game" is a performance decision, not a claim about what this app is. Vendor
performance layers read it: on the OxygenOS test device it moves the app onto the boosted
path, where the foreground CPU ceiling goes from 1.9/1.65 GHz to the hardware maximum of
3.32/3.80 GHz — measured before and after, with the app in the foreground. Decode is the
most CPU-hungry thing a phone can do outside a game, so the boosted path is the right one.
It cannot be a runtime setting: a manifest attribute is fixed at install time, and the
only lever would be a per-flavor manifest. The effect is the vendor's, not Android's:
neutral on stock builds, and Samsung's game service has historically THROTTLED what it
classifies this way, so treat any per-device number as a measurement rather than a rule. -->
<application
android:allowBackup="false"
android:appCategory="game"
android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"