Give a phone without a Qualcomm SoC the generic WebGPU runtime

The APK's ONNX Runtime is the QNN build, which carries no WebGPU, so a
non-Qualcomm phone had nothing above the CPU provider. The APK now also
carries Microsoft's stock onnxruntime-android 1.29.0 (32 MB) as
libonnxruntime_generic.so, and the app offers it after the QNN build.

The runtime search stops at a perfect fit, so on a Qualcomm device the
QNN build — listed first — is all that is opened, and the generic build
never loads beside it. A Qualcomm SoC is read from ro.soc.manufacturer
or, before Android 12, from Qualcomm's FastRPC library being present:
a Qualcomm device mistaken for another would trade its Hexagon for the
generic rung.
This commit is contained in:
2026-10-04 21:00:15 -04:00
parent cb97ebe7ac
commit 43402bfe5b
6 changed files with 59 additions and 19 deletions
+8 -1
View File
@@ -58,7 +58,10 @@ pub fn ensure_installed() {
/// Best fit, not first found (§3.2): a device can hold several runtimes —
/// the package's OpenVINO build, a CUDA build the user fetched, the
/// distribution's ROCm build — and each carries one vendor's providers.
/// Between equals, the earlier directory wins, as it always has.
/// Between equals, the earlier directory wins, as it always has, and a
/// runtime that fits perfectly ends the search: the APK's QNN build on a
/// Qualcomm tablet is found first, and the generic build beside it is
/// never opened there.
/// `DARKROOM_ORT_DIR`, when it loads, wins outright: it is how a person
/// says which runtime they mean.
pub fn install(dirs: &[PathBuf]) -> Runtime {
@@ -110,7 +113,11 @@ fn install_best(dirs: &[PathBuf]) -> Option<Runtime> {
found.push(f);
break;
}
let perfect = gpus.score(&f.providers) >= crate::hardware::PERFECT;
found.push(f);
if perfect {
break;
}
}
Err(e) => log::info!("inference: no runtime in {}: {e}", dir.display()),
}
+21 -5
View File
@@ -19,6 +19,10 @@ pub struct Gpus {
pub qualcomm: bool,
}
/// The score of a runtime whose vendor rung matches the device's GPU.
/// Nothing beats it, so the search stops there.
pub const PERFECT: u32 = 3;
impl Gpus {
/// How well a runtime offering `providers` fits this device. The vendor
/// rungs score above OpenVINO because a machine with an Intel iGPU and
@@ -29,10 +33,10 @@ impl Gpus {
providers
.iter()
.map(|p| match p.as_str() {
"TensorrtExecutionProvider" | "CUDAExecutionProvider" if self.nvidia => 3,
"MIGraphXExecutionProvider" if self.amd_rocm => 3,
"QNNExecutionProvider" if self.qualcomm => 3,
"CoreMLExecutionProvider" => 3,
"TensorrtExecutionProvider" | "CUDAExecutionProvider" if self.nvidia => PERFECT,
"MIGraphXExecutionProvider" if self.amd_rocm => PERFECT,
"QNNExecutionProvider" if self.qualcomm => PERFECT,
"CoreMLExecutionProvider" => PERFECT,
"OpenVINOExecutionProvider" if self.intel => 2,
"WebGpuExecutionProvider" => 1,
_ => 0,
@@ -91,9 +95,21 @@ pub fn detect() -> Gpus {
#[cfg(target_os = "android")]
pub fn detect() -> Gpus {
// `ro.soc.manufacturer` exists from Android 12. Qualcomm's FastRPC
// library, which the Hexagon path loads anyway, is the second witness:
// a Qualcomm device read as anything else would trade its NPU for the
// generic rung.
let soc = crate::probe::system_property("ro.soc.manufacturer");
let fastrpc = [
"/vendor/lib64/libcdsprpc.so",
"/system/vendor/lib64/libcdsprpc.so",
]
.iter()
.any(|p| std::path::Path::new(p).exists());
Gpus {
qualcomm: soc.eq_ignore_ascii_case("QTI") || soc.eq_ignore_ascii_case("Qualcomm"),
qualcomm: soc.eq_ignore_ascii_case("QTI")
|| soc.eq_ignore_ascii_case("Qualcomm")
|| fastrpc,
..Gpus::default()
}
}