Skip to content

Commit 8d1d73a

Browse files
[BCI] Enable dynamic backend loading for Android (QVAC-19235) (#2326)
* bci-whispercpp: thread backendsDir + Android dynamic-backend loader (QVAC-19235) PR 1 of 3 for the BCI GPU rollout. Behaviour on every platform is unchanged today because `bci-whispercpp` still pins `whisper-cpp@1.8.4.2`, whose port builds ggml with the static-backend registry (`GGML_BACKEND_DL=OFF`). This PR is the safety net that lets the follow-up `whisper-cpp@1.8.5` bump (QVAC-19009) flip `GGML_BACKEND_DL=ON` on Android without reproducing the `SIGABRT` on model load that hit `transcription-whispercpp` on its PR #2124 -- see `aiDocs/15-android-mobile-test-crash-fix.md` for the post-mortem. What changes: - BCIConfig gains a `backendsDir` string field. JSAdapter reads the top-level `configurationParams.backendsDir` directly (not via `loadMap`) to avoid polluting any of the BCI handler namespaces. - index.js threads `BCIWhispercppConfig.backendsDir` into `configurationParams`, defaulting to `<addon>/prebuilds` resolved via `bare-path`. Surfaced in `index.d.ts`. - BCIModel::load() now calls `ensureBackendsLoadedAndroid()` (gated on `__ANDROID__`, idempotent via `std::call_once`) BEFORE `whisper_init_from_file_with_params()`. Joins `backendsDir` with the compile-time `BACKENDS_SUBDIR` and dispatches to `ggml_backend_load_all_from_path()`. Inactive today (no MODULE backends produced at 1.8.4.2); the call still runs because it's cheap and we want symmetry with transcription/parakeet. - BCIModel::captureActiveBackendInfo() snapshots the active backend identity + device-memory after backend registration. New `RuntimeStats` keys: `backendDevice`, `backendId`, `gpuMemTotalMb`, `gpuMemFreeMb`. Numeric mapping (CPU=0 / Metal=1 / CUDA=2 / Vulkan=3 / OpenCL=4 / other=99) lock-stepped with transcription-whispercpp 0.9.0 + transcription-parakeet for cross-addon Device Farm comparability. - CMakeLists.txt: `bare_target` + `bare_module_target` discovery, `BACKENDS_SUBDIR` compile define, `BACKEND_DL_LIBS` (IMPORTED `ggml::*` targets) + `BACKEND_DL_LOOSE_SOS` (loose `libqvac-speech-ggml-*.so` staging) plumbing. Parity with transcription-whispercpp / transcription-parakeet. Test-then-implementation: GoogleTest additions guard the new config field round-trip and the new RuntimeStats key contract + default-CPU values (mirrors `WhisperModel`'s `BackendInfo` unit- test contract). All 22 GTests, 21 brittle unit tests, 6 integration tests pass locally on Linux. Version bump: 0.1.0 -> 0.1.1 (additive plumbing only, no behaviour change; the minor bump is reserved for PR 3 / GPU enablement). Co-authored-by: Cursor <cursoragent@cursor.com> * bci-whispercpp: derive backend stats from actual whisper_context_params (review) Addresses jpgaribotti's review on #2326: captureActiveBackendInfo() treated a missing contextParams.use_gpu as false, but toWhisperContextParams() leaves whisper_context_default_params() intact (use_gpu=true). So with no explicit use_gpu, whisper could initialise a GPU while runtimeStats reported backendDevice=0/backendId=0. Fix: pass the exact whisper_context_params used by whisper_init_from_file_with_params() into captureActiveBackendInfo() and read use_gpu / gpu_device from it, instead of re-deriving from the BCIConfig map with a (wrong) false default. Reported backend identity is now in lock-step with the context whisper actually built. Also corrects the GPU device selection to mirror whisper's whisper_backend_init_gpu(): gpu_device is an index AMONG GPU devices (default 0), not an index into all devices (device 0 is normally the CPU). Removed the now-unused configUseGpu()/configGpuDeviceIndex() helpers. Validated: host build + test:cpp (22/22) + test:integration (6/6) green; clang-tidy + clang-format clean on the changed file; android-arm64 cross-build compiles. Co-authored-by: Cursor <cursoragent@cursor.com> * bci-whispercpp: report IGPU backends + Adreno OpenCL guard (port transcription #2343) Ports the transcription-whispercpp fix merged in #2343 ("report IGPU backends (fix Mali silent CPU mislabel)") into the BCI addon so the two stay faithful mirrors and the BCI Android GPU path (QVAC-19234) behaves correctly on real devices: - captureActiveBackendInfo() now walks BOTH GGML_BACKEND_DEVICE_TYPE_GPU and _IGPU. ggml-vulkan reports integrated GPUs (Mali, Adreno-via- Vulkan, Intel iGPU) as IGPU; skipping IGPU made every Vulkan-on-mobile device look like a silent CPU fallback even while whisper ran on the GPU. gpu_device is treated as an index into the filtered GPU/IGPU list, matching whisper_backend_init_gpu()'s `cnt`. - New adrenoOpenclGpuDeviceIndex() + an Adreno guard in load(): when an Adreno OpenCL device is registered (Android registers both Vulkan and OpenCL for the same GPU, Vulkan first), steer whisper to OpenCL. The Adreno Vulkan driver SIGSEGVs in ggml compute (vkCmdBindPipeline); OpenCL is the supported Adreno backend. No-op on Mali/desktop. - captureActiveBackendInfo() signature is now (bool useGpu, int gpuDeviceIndex), fed the exact post-guard whisper_context_params, and the fallback WARNING text matches #2343. All behaviour-neutral on this PR's whisper-cpp@1.8.4.2 (no GPU/IGPU devices register, adrenoOpenclGpuDeviceIndex()=-1). Validated: host build + test:cpp (22/22) + test:integration (6/6) + lint + test:dts; clang-format-20 + clang-tidy-19 clean on the changed files; android-arm64 NDK cross-build compiles. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 4bc3e10 commit 8d1d73a

10 files changed

Lines changed: 553 additions & 5 deletions

File tree

packages/bci-whispercpp/CHANGELOG.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,57 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.1.2]
9+
10+
Android dynamic-backend-loading infrastructure (QVAC-19235). Behaviour
11+
on every platform is unchanged today because `bci-whispercpp` still
12+
pins `whisper-cpp@1.8.4.2`, whose port builds ggml with the static-
13+
backend registry (`GGML_BACKEND_DL=OFF`). This PR is the "safety net"
14+
that lets the follow-up `whisper-cpp@1.8.5` bump (QVAC-19009) flip
15+
`GGML_BACKEND_DL=ON` on Android without reproducing the `SIGABRT` on
16+
model load that hit `transcription-whispercpp` on its PR #2124. See
17+
`aiDocs/15-android-mobile-test-crash-fix.md` for the post-mortem.
18+
19+
### Added
20+
21+
- Native `BCIConfig::backendsDir` field plus JS-side `configurationParams.backendsDir`
22+
pass-through (defaults to `<addon>/prebuilds` resolved via
23+
`bare-path`). Surfaces on `BCIWhispercppConfig.backendsDir`.
24+
- Android-only `ensureBackendsLoadedAndroid()` in `BCIModel::load()`
25+
(process-local `std::call_once`); resolves the per-arch backend
26+
subdir from `backendsDir / BACKENDS_SUBDIR` and dispatches to
27+
`ggml_backend_load_all_from_path()`.
28+
- `captureActiveBackendInfo(useGpu, gpuDevice)` in `BCIModel::load()`:
29+
enumerates `ggml_backend_dev_*` after backend registration and
30+
snapshots the active backend identity + device memory. New
31+
`RuntimeStats` keys: `backendDevice`, `backendId`, `gpuMemTotalMb`,
32+
`gpuMemFreeMb`. The numeric mapping (CPU=0 / Metal=1 / CUDA=2 /
33+
Vulkan=3 / OpenCL=4 / other=99) is lock-stepped with
34+
`transcription-whispercpp 0.9.0` and `transcription-parakeet` for
35+
cross-addon Device Farm comparability. Backend selection is sourced
36+
from the exact `whisper_context_params` the context was built with
37+
(use_gpu/gpu_device), walks the `whisper_backend_init_gpu()`-filtered
38+
GPU **and IGPU** device list (Mali / Adreno-via-Vulkan / Intel iGPU
39+
report as IGPU), and applies the Adreno OpenCL preference — mirroring
40+
`transcription-whispercpp` PR #2270 + #2343. Inert on
41+
`whisper-cpp@1.8.4.2` (no GPU backends registered).
42+
- `CMakeLists.txt`: `bare_target` + `bare_module_target` discovery,
43+
`BACKENDS_SUBDIR` compile define, `BACKEND_DL_LIBS` (IMPORTED
44+
`ggml::*` targets) + `BACKEND_DL_LOOSE_SOS` (loose
45+
`libqvac-speech-ggml-*.so` staging) plumbing, parity with
46+
`transcription-whispercpp` / `transcription-parakeet`. Inactive
47+
today (no MODULE backends produced at `whisper-cpp@1.8.4.2`);
48+
activates on the QVAC-19009 bump.
49+
50+
### Added (tests)
51+
52+
- `BCIConfig.backendsDirDefaultsEmpty`, `BCIConfig.backendsDirRoundTrip`:
53+
guard the new config field's defaults and copy semantics.
54+
- `BCIModel.runtimeStatsExposesBackendIdentityKeys`,
55+
`BCIModel.backendIdentityDefaultsToCPU`: guard the new
56+
`RuntimeStats` keys + default-CPU contract without requiring a
57+
loaded model (mirrors transcription-whispercpp's `BackendInfo`
58+
unit-test pattern).
859
## [0.1.1] - 2026-06-02
960

1061
### Changed

packages/bci-whispercpp/CMakeLists.txt

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,63 @@ if(WIN32)
4545
add_definitions(-DNOMINMAX -DWIN32_LEAN_AND_MEAN -DNOGDI)
4646
endif()
4747

48-
add_bare_module(bci-whispercpp EXPORTS)
48+
# Subdir bare's runtime installs the dlopen-able backend modules into
49+
# (<addon>/prebuilds/<bare_target>/<module_name>/); BCIModel joins it
50+
# with the runtime-supplied backendsDir before
51+
# ggml_backend_load_all_from_path(). Mirrors transcription-whispercpp.
52+
bare_target(bare_target_value)
53+
bare_module_target("." _unused_target NAME module_name VERSION _unused_version)
54+
set(BACKENDS_SUBDIR_VALUE "${bare_target_value}/${module_name}")
55+
56+
# Collect every dynamically-loadable ggml backend that the `ggml-speech`
57+
# port produced ($CURRENT_PACKAGES_DIR/lib/libqvac-speech-ggml-*.so on
58+
# Android; nothing on Apple/static-only platforms). Mirrors
59+
# qvac/packages/transcription-whispercpp/CMakeLists.txt verbatim --
60+
# two branches:
61+
# 1. IMPORTED targets that `find_package(ggml)` already surfaced
62+
# (CPU on Android, all backends on Linux desktop).
63+
# 2. Loose `libqvac-speech-ggml-*.so` MODULE files that ggml-config
64+
# deliberately omits from GGML_AVAILABLE_BACKENDS' IMPORTED set
65+
# (Vulkan + OpenCL on Android -- the loader picks them up at
66+
# runtime via `ggml_backend_load_all_from_path()`).
67+
set(BACKEND_DL_LIBS "")
68+
if((ANDROID OR UNIX) AND NOT APPLE)
69+
foreach(_backend ${GGML_AVAILABLE_BACKENDS})
70+
if(TARGET ggml::${_backend})
71+
list(APPEND BACKEND_DL_LIBS INSTALL TARGET ggml::${_backend})
72+
endif()
73+
endforeach()
74+
endif()
75+
76+
set(BACKEND_DL_LOOSE_SOS "")
77+
if((ANDROID OR UNIX) AND NOT APPLE)
78+
if(NOT VCPKG_INSTALLED_PATH OR NOT EXISTS "${VCPKG_INSTALLED_PATH}/lib")
79+
message(WARNING "bci-whispercpp: VCPKG_INSTALLED_PATH "
80+
"('${VCPKG_INSTALLED_PATH}') has no lib/ dir; skipping loose "
81+
"ggml backend .so pickup -- the runtime may fail to find "
82+
"Vulkan / OpenCL backends.")
83+
else()
84+
file(GLOB BACKEND_DL_LOOSE_SOS
85+
"${VCPKG_INSTALLED_PATH}/lib/libqvac-speech-ggml-*.so")
86+
foreach(_lib IN LISTS BACKEND_DL_LOOSE_SOS)
87+
message(STATUS "bci-whispercpp: will stage ggml backend ${_lib}")
88+
endforeach()
89+
endif()
90+
endif()
91+
92+
add_bare_module(bci-whispercpp EXPORTS ${BACKEND_DL_LIBS})
93+
94+
# Stage every loose MODULE backend (Vulkan / OpenCL on Android, ...)
95+
# into the same per-host/per-module subdir cmake-bare's INSTALL TARGET
96+
# branch uses for IMPORTED targets -- keeping destinations identical so
97+
# `ggml_backend_load_all_from_path(backendsDir / BACKENDS_SUBDIR)`
98+
# discovers IMPORTED-target CPU + loose-file GPU backends in one scan.
99+
if(BACKEND_DL_LOOSE_SOS)
100+
install(
101+
FILES ${BACKEND_DL_LOOSE_SOS}
102+
DESTINATION ${bare_target_value}/${module_name}
103+
)
104+
endif()
49105

50106
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
51107
target_link_options(${bci-whispercpp}_module PRIVATE -Wl,--exclude-libs,ALL)
@@ -77,6 +133,7 @@ target_link_libraries(
77133
)
78134

79135
target_compile_definitions(${bci-whispercpp} PUBLIC JS_LOGGER)
136+
target_compile_definitions(${bci-whispercpp} PRIVATE BACKENDS_SUBDIR="${BACKENDS_SUBDIR_VALUE}")
80137

81138
if(WIN32)
82139
target_link_libraries(

packages/bci-whispercpp/addon/src/js-interface/JSAdapter.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,16 @@ BCIConfig JSAdapter::loadFromJSObject(Object jsObject, js_env_t* env) {
105105
loadBCIParams(bciConfigObj.value(), env, config);
106106
}
107107

108+
// Top-level `configurationParams.backendsDir`. Read directly (not through
109+
// `loadMap` / handler dispatch) because routing it through any of the
110+
// sub-section maps would either throw on an unrecognised key or pollute
111+
// a config namespace the BCI handlers don't own. Consumed by
112+
// `BCIModel::load()` on Android only; ignored on other platforms.
113+
auto backendsDirJs = jsObject.getOptionalProperty<String>(env, "backendsDir");
114+
if (backendsDirJs.has_value()) {
115+
config.backendsDir = backendsDirJs.value().as<std::string>(env);
116+
}
117+
108118
return config;
109119
}
110120

packages/bci-whispercpp/addon/src/model-interface/bci/BCIConfig.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ struct BCIConfig {
2727
std::map<std::string, JSValueVariant> whisperContextCfg;
2828
std::map<std::string, JSValueVariant> bciConfig;
2929

30+
// Addon prebuilds folder (`configurationParams.backendsDir` from JS).
31+
// Combined with the compile-time `BACKENDS_SUBDIR` to locate the
32+
// per-arch ggml `.so` modules for `ggml_backend_load_all_from_path()`.
33+
// Android-only; empty elsewhere. Mirrors WhisperConfig::backendsDir
34+
// in transcription-whispercpp 0.9.0.
35+
std::string backendsDir;
36+
3037
// Owned storage for string values that whisper_full_params references by
3138
// pointer (e.g. p.language = lang_.c_str()). Must outlive the params struct.
3239
mutable std::string lang_;

0 commit comments

Comments
 (0)