Commit: f8c421e
Parent: 5ef665c

Switch execution provider from OpenVINO to WebGPU

Mårten Åsberg committed on 2026-08-03 at 13:02
OpenVINO's GPU plugin couldn't compile LightGlue's graph (its dynamic
keypoint-count axis kept crashing ahead-of-time shape compilation on
GPU, even with with_dynamic_shapes enabled) and required a painful
from-source ONNX Runtime build with manual linking on top of that.

WebGPU is one of ort's exact prebuilt binary combinations for Linux
x86_64, so this drops the custom build entirely -- plain `cargo build
--release` now suffices. It's also vendor/platform portable (Vulkan on
Linux) rather than Intel-only. Device::Cpu now simply registers no
execution provider, since WebGPU has no CPU-plugin equivalent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cargo.lock +7 -0
diff --git a/Cargo.lock b/Cargo.lock
index ad34142..8000b8d 100644
@@ -610,6 +610,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f22fb22f065b308be0d8724e3706c7fa3fc2a6c7d6899df4cad7860e7a75436"
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1087,6 +1093,7 @@ version = "2.0.0-rc.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf211e3776eea6aec988552fa118dd746d70e1b1e5e244058d1c98015f3e5872"
dependencies = [
"glob",
"hmac-sha256",
"lzma-rust2",
"ureq",
Cargo.toml +1 -1
diff --git a/Cargo.toml b/Cargo.toml
index 224674b..74e1703 100644
@@ -10,7 +10,7 @@ clap = { version = "4.6.5", features = ["derive"] }
image = "0.25.10"
nalgebra = "0.35.0"
ndarray = "0.17.2"
ort = { version = "2.0.0-rc.13", features = ["openvino"] }
ort = { version = "2.0.0-rc.13", features = ["webgpu"] }
rand = "0.10.2"
rayon = "1.12.0"
rusqlite = { version = "0.37", features = ["bundled"] }
src/exhaustive_matcher.rs +1 -2
diff --git a/src/exhaustive_matcher.rs b/src/exhaustive_matcher.rs
index bdf426a..3cc6929 100644
@@ -73,8 +73,7 @@ fn run_lightglue(
let n = a.keypoints.len();
let m = b.keypoints.len();
// Below this, RANSAC could never fit a fundamental matrix (needs >= 8 correspondences)
// so the pair can never pass verification anyway. Also sidesteps a shape-inference crash
// in OpenVINO EP's subgraph partitioning on degenerate (e.g. single-keypoint) sequences.
// so the pair can never pass verification anyway.
if n < ransac::MIN_CORRESPONDENCES || m < ransac::MIN_CORRESPONDENCES {
return Ok(Vec::new());
}
src/models.rs +16 -28
diff --git a/src/models.rs b/src/models.rs
index fae63ba..0a0586a 100644
@@ -11,42 +11,30 @@ pub enum Device {
Gpu,
}
impl Device {
fn openvino_device_type(self) -> &'static str {
match self {
Device::Cpu => "CPU",
Device::Gpu => "GPU",
}
}
}
/// Loads an ONNX model with the OpenVINO execution provider targeting `device`.
/// Registration failures (e.g. no Arc GPU present) are logged and the session
/// silently falls back to the CPU execution provider, per `ort`'s default behavior.
/// Loads an ONNX model, registering the WebGPU execution provider when `device` is
/// `Gpu` (portable across vendors/platforms via Vulkan/D3D12/Metal). `Cpu` registers
/// no execution provider at all, so ONNX Runtime uses its built-in default CPU EP.
pub fn load_session(model_path: &Path, device: Device) -> Result<Session> {
let openvino = ep::OpenVINO::default()
.with_device_type(device.openvino_device_type())
// LightGlue's keypoint-count axis is dynamic (paired with any extractor/keypoint
// count). OpenVINO EP compiles subgraphs ahead-of-time by default, which can
// mis-propagate that dynamic axis through the attention stack and bake in a wrong
// placeholder shape; this defers shape resolution to each request's actual input
// shape instead. Harmless for ALIKED, whose shapes are all static anyway.
.with_dynamic_shapes(true);
let mut execution_providers = Vec::new();
match openvino.is_available() {
Ok(true) => {}
Ok(false) => eprintln!(
"OpenVINO execution provider not available; falling back to CPU for {}",
model_path.display()
),
Err(e) => eprintln!("Could not query OpenVINO EP availability: {e}"),
if device == Device::Gpu {
let webgpu = ep::WebGPU::default();
match webgpu.is_available() {
Ok(true) => {}
Ok(false) => eprintln!(
"WebGPU execution provider not available; falling back to CPU for {}",
model_path.display()
),
Err(e) => eprintln!("Could not query WebGPU EP availability: {e}"),
}
execution_providers.push(webgpu.build());
}
Session::builder()?
// `.with_execution_providers` returns an error type that carries the SessionBuilder
// (for `.recover()`), which holds raw FFI pointers and so isn't Send+Sync; anyhow
// requires Send+Sync, so convert explicitly instead of using `?` directly.
.with_execution_providers([openvino.build()])
.with_execution_providers(execution_providers)
.map_err(|e| anyhow!("{e}"))?
.commit_from_file(model_path)
.with_context(|| format!("loading ONNX model from {}", model_path.display()))