From 95c9cffc0d4ede6baa9ca02fb59d5b6bbc1bf673 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sat, 19 Sep 2026 16:21:53 +0200 Subject: [PATCH] Keep the embedder off the Hexagon, and let the probe example ask for a runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On the tablet the engine compiled arcface for the NPU: the routing compared the form a rung wants with the form on offer, and for the embedder both are f32, so nothing said no. A rung now says which roles it serves at all, and the Hexagon does not serve the embedder (§7 — its vectors must compare across devices). Tested at the routing seam. dr-segment's onnx_probe example still named ort-tract, which is what stopped the workspace test build. --- core/dr-inference-engine/src/engines.rs | 3 +- core/dr-inference-engine/src/lib.rs | 49 ++++++++++++++++++++++--- core/dr-segment/Cargo.toml | 3 ++ core/dr-segment/examples/onnx_probe.rs | 2 +- 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/core/dr-inference-engine/src/engines.rs b/core/dr-inference-engine/src/engines.rs index 2502255..a444adf 100644 --- a/core/dr-inference-engine/src/engines.rs +++ b/core/dr-inference-engine/src/engines.rs @@ -55,6 +55,7 @@ pub fn run() { let mut jobs: Vec<(crate::Role, Source, u64)> = cfg .models .iter() + .filter(|(role, _)| rung.serves(*role)) .filter_map(|(role, path)| { let (path, form) = crate::resolve_model(*role, path); (form == rung.form(*role)).then(|| { @@ -65,7 +66,7 @@ pub fn run() { .chain(cfg.embedded.iter().filter_map(|(role, bytes)| { // An embedded model has no int8 sibling to offer a rung that // wants one; it runs on that rung's fallback. - (rung.form(*role) == Form::F32).then_some(( + (rung.serves(*role) && rung.form(*role) == Form::F32).then_some(( *role, Source::Bytes(bytes), bytes.len() as u64, diff --git a/core/dr-inference-engine/src/lib.rs b/core/dr-inference-engine/src/lib.rs index e44fe1d..5e726e9 100644 --- a/core/dr-inference-engine/src/lib.rs +++ b/core/dr-inference-engine/src/lib.rs @@ -95,12 +95,21 @@ impl Rung { /// The model form this rung wants for a role. fn form(self, role: Role) -> Form { - match (self, role) { - (Rung::Hexagon, Role::Embedder) => Form::F32, - (Rung::Hexagon, _) => Form::Int8, + match self { + Rung::Hexagon => Form::Int8, _ => Form::F32, } } + + /// Whether this rung runs `role` at all. The Hexagon takes int8 graphs + /// only, and the embedder is never int8 (§7) — it runs on the CPU + /// beside a detector on the NPU, so its vectors compare across devices. + fn serves(self, role: Role) -> bool { + match self { + Rung::Hexagon => role != Role::Embedder, + _ => true, + } + } } /// How long a session outlives its last use unless [`Config::decay`] says @@ -407,7 +416,7 @@ fn current_rung(s: &State) -> Rung { /// caller adds [`form_suffix`] to the `model_id` it records. pub fn resolve_model(role: Role, canonical: &Path) -> (PathBuf, Form) { let rung = current_rung(&state().lock().unwrap()); - if rung.form(role) == Form::Int8 { + if rung.serves(role) && rung.form(role) == Form::Int8 { let sibling = int8_sibling(canonical); if sibling.is_file() { return (sibling, Form::Int8); @@ -451,7 +460,7 @@ pub fn open(role: Role, form: Form, bytes: &[u8]) -> Result { /// the form on offer, or a missing engine says one lower (§6 step 4). fn effective_rung(s: &State, selected: Rung, role: Role, form: Form, bytes: &[u8]) -> Rung { let mut rung = selected; - if rung.form(role) != form { + if !rung.serves(role) || rung.form(role) != form { // The embedder on a Hexagon device, or an f32 detector where the int8 // sibling was missing: neither can go to the NPU. rung = rung.fallback(); @@ -541,6 +550,36 @@ mod tests { assert_eq!(loaded(), 1); } + #[test] + fn the_hexagon_never_takes_the_embedder() { + assert!(!Rung::Hexagon.serves(Role::Embedder)); + assert!(Rung::Hexagon.serves(Role::Detector)); + assert_eq!(Rung::Hexagon.form(Role::Detector), Form::Int8); + // A detector offered in f32 on a Hexagon device lands on the CPU. + let s = State { + config: Config::default(), + cache: Cache { + rung: Some(Rung::Hexagon), + ..Cache::default() + }, + probing: false, + wanted: 0, + }; + assert_eq!( + effective_rung(&s, Rung::Hexagon, Role::Embedder, Form::F32, b""), + Rung::Cpu + ); + assert_eq!( + effective_rung(&s, Rung::Hexagon, Role::Detector, Form::F32, b""), + Rung::Cpu + ); + // An int8 detector whose context is not compiled yet: also the CPU. + assert_eq!( + effective_rung(&s, Rung::Hexagon, Role::Detector, Form::Int8, b""), + Rung::Cpu + ); + } + #[test] fn the_status_line_reads_as_the_floor_before_init() { let s = status(); diff --git a/core/dr-segment/Cargo.toml b/core/dr-segment/Cargo.toml index 6f0f021..ff29e15 100644 --- a/core/dr-segment/Cargo.toml +++ b/core/dr-segment/Cargo.toml @@ -24,6 +24,9 @@ ndarray = { workspace = true, optional = true } # tree for embedded previews. zune-jpeg.workspace = true env_logger.workspace = true +# The probe example drives `ort` directly to print the raw load error, and +# asks the engine for a runtime by name rather than naming one itself. +dr-inference-engine.workspace = true [features] # On by default: a local adjustment that cannot select a subject is half the diff --git a/core/dr-segment/examples/onnx_probe.rs b/core/dr-segment/examples/onnx_probe.rs index ea1c8ad..1dbdc41 100644 --- a/core/dr-segment/examples/onnx_probe.rs +++ b/core/dr-segment/examples/onnx_probe.rs @@ -34,7 +34,7 @@ fn main() { let bytes = std::fs::read(&path).expect("read model"); println!("{path}: {} bytes", bytes.len()); - let _ = ort::set_api(ort_tract::api()); + dr_inference_engine::ensure_runtime(); let t = Instant::now(); let mut session = match ort::session::Session::builder().and_then(|mut b| b.commit_from_memory(&bytes)) {