Skip to content

Commit f6f67b5

Browse files
committed
sync: update from the development repository
Content snapshot from the private development repository. The two histories are independent by design; this commit carries the file contents, not the upstream commits.
1 parent 9104bcf commit f6f67b5

7 files changed

Lines changed: 133 additions & 50 deletions

File tree

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@
1313
*.swo
1414
.idea/
1515
.vscode/
16+
17+
# MCP server registration: names machine-local binaries, not shared
18+
.mcp.json

‎yu-hailo-infer-core/Cargo.toml‎

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,32 @@ repository.workspace = true
1111
authors.workspace = true
1212

1313
[features]
14-
# Enable CUDA Execution Provider (requires CUDA-enabled ORT binary or ORT_DYLIB_PATH).
14+
# Execution providers, off by default. Whether a provider needs `load-dynamic`
15+
# is decided by ort-sys's own prebuilt table (ort-sys/build/download/dist.txt):
16+
# its resolver only varies the downloaded binary for `cuda`/`tensorrt`,
17+
# `nvrtx`, `webgpu`, `training` and `rocm`, and only ships feature sets
18+
# `none`, `cu12`, `cu13`, `nvrtx` and `wgpu`. So:
19+
# - cuda resolves to a real CUDA-enabled download (cu12/cu13, picked from
20+
# CUDA_HOME/nvcc), and needs nothing else.
21+
# - coreml is not in the resolver at all, so it links the plain `none`
22+
# download -- which on Apple targets is an ORT build with CoreML compiled
23+
# in. No separate library to point at.
24+
# - rocm IS in the resolver but no prebuilt row carries it, so resolution
25+
# falls back to `none` with a warning and the provider would fail to
26+
# register at runtime. load-dynamic is the only way to reach a real ROCm
27+
# build here.
28+
# - directml and openvino are absent from both the resolver and every
29+
# prebuilt feature set, so they too need an externally supplied library.
30+
# load-dynamic means the ONNX Runtime library is found at runtime via
31+
# ORT_DYLIB_PATH instead of being linked at build time.
1532
cuda = ["ort/cuda"]
16-
# Enable ROCm Execution Provider (requires ROCm-enabled ORT binary or ORT_DYLIB_PATH).
17-
rocm = ["ort/rocm"]
18-
# Enable DirectML Execution Provider on Windows. Temporary build/runtime fallback.
33+
# ROCm has no prebuilt binary; point ORT_DYLIB_PATH at a --use_rocm build.
34+
rocm = ["ort/rocm", "ort/load-dynamic"]
35+
# DirectML on Windows; point ORT_DYLIB_PATH at a DirectML-enabled build.
1936
directml = ["ort/directml", "ort/load-dynamic"]
20-
# Enable CoreML Execution Provider on macOS (Apple Silicon / Intel Mac).
37+
# CoreML on macOS (Apple Silicon / Intel Mac); served by the default download.
2138
coreml = ["ort/coreml"]
22-
# Enable OpenVINO Execution Provider (Intel GPU/VPU on Linux).
39+
# OpenVINO (Intel GPU/NPU); point ORT_DYLIB_PATH at an OpenVINO-enabled build.
2340
openvino = ["ort/openvino", "ort/load-dynamic"]
2441

2542
[dependencies]

‎yu-hailo-infer-core/src/clip_image.rs‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ use std::{path::Path, sync::Mutex};
33
use ndarray::Array4;
44
use ort::{session::Session, value::Value};
55

6-
use crate::{clip::validate_and_normalize, InferError};
6+
use crate::{clip::validate_and_normalize, ep::build_session, InferError};
77

88
const IMAGE_WIDTH: u32 = 224;
99
const IMAGE_HEIGHT: u32 = 224;
1010
const MEAN: [f32; 3] = [0.48145466, 0.4578275, 0.40821073];
1111
#[allow(clippy::excessive_precision)] // CLIP's published preprocessing constants.
1212
const STD: [f32; 3] = [0.26862954, 0.26130258, 0.27577711];
1313

14-
/// CPU ONNX Runtime encoder for Xenova's CLIP ViT-B/16 vision model.
14+
/// ONNX Runtime encoder for Xenova's CLIP ViT-B/16 vision model.
1515
pub struct ClipImageEncoder {
1616
session: Mutex<Session>,
1717
}
@@ -22,9 +22,7 @@ impl ClipImageEncoder {
2222
return Err(InferError::ModelNotDownloaded(model_dir.to_owned()));
2323
}
2424
Ok(Self {
25-
session: Mutex::new(
26-
Session::builder()?.commit_from_file(model_dir.join("vision_model.onnx"))?,
27-
),
25+
session: Mutex::new(build_session(&model_dir.join("vision_model.onnx"))?),
2826
})
2927
}
3028

‎yu-hailo-infer-core/src/clip_text.rs‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ use tokenizers::{
77
TruncationStrategy,
88
};
99

10-
use crate::{clip::validate_and_normalize, InferError};
10+
use crate::{clip::validate_and_normalize, ep::build_session, InferError};
1111

1212
const MAX_SEQUENCE_LENGTH: usize = 77;
1313

14-
/// CPU ONNX Runtime encoder for Xenova's CLIP ViT-B/16 text model.
14+
/// ONNX Runtime encoder for Xenova's CLIP ViT-B/16 text model.
1515
pub struct ClipTextEncoder {
1616
session: Mutex<Session>,
1717
tokenizer: Tokenizer,
@@ -43,16 +43,11 @@ impl ClipTextEncoder {
4343
}));
4444

4545
Ok(Self {
46-
session: Mutex::new(Self::build_session(&model_dir.join("text_model.onnx"))?),
46+
session: Mutex::new(build_session(&model_dir.join("text_model.onnx"))?),
4747
tokenizer,
4848
})
4949
}
5050

51-
fn build_session(model_path: &Path) -> Result<Session, InferError> {
52-
// ORT uses CPUExecutionProvider by default when no provider list is supplied.
53-
Ok(Session::builder()?.commit_from_file(model_path)?)
54-
}
55-
5651
pub fn encode(&self, text: &str) -> Result<Vec<f32>, InferError> {
5752
let encoding = self
5853
.tokenizer

‎yu-hailo-infer-core/src/engine.rs‎

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use ort::{session::Session, value::Value};
66
use serde::{Deserialize, Serialize};
77

88
use crate::{
9+
ep::build_session,
910
profile::WdProfileSpec,
1011
tags::{load_tags, TagMeta},
1112
InferError,
@@ -106,7 +107,7 @@ impl WdInferEngine {
106107
if !model_path.exists() {
107108
return Err(InferError::ModelNotDownloaded(model_dir.to_owned()));
108109
}
109-
let session = Self::build_session(&model_path)?;
110+
let session = build_session(&model_path)?;
110111
let tag_meta = load_tags(
111112
model_dir,
112113
&spec.tag_source,
@@ -126,36 +127,6 @@ impl WdInferEngine {
126127
})
127128
}
128129

129-
/// Build an ORT session, trying GPU EPs in priority order then falling back to CPU.
130-
///
131-
/// Compile with `--features cuda` or `--features rocm` to enable GPU paths.
132-
/// GPU EPs fail gracefully at runtime if the hardware/driver is absent.
133-
fn build_session(model_path: &Path) -> Result<Session, InferError> {
134-
#[cfg(any(feature = "cuda", feature = "rocm"))]
135-
{
136-
let mut eps: Vec<ort::execution_providers::ExecutionProviderDispatch> = Vec::new();
137-
#[cfg(feature = "cuda")]
138-
{
139-
tracing::info!("Requesting CUDA execution provider");
140-
eps.push(ort::ep::CUDA::default().build());
141-
}
142-
#[cfg(feature = "rocm")]
143-
{
144-
tracing::info!("Requesting ROCm execution provider");
145-
eps.push(ort::ep::ROCm::default().build());
146-
}
147-
// CPU must be explicit — with_execution_providers replaces the default EP list
148-
// and does not automatically append CPU. ORT tries EPs in order; CPU is the fallback.
149-
eps.push(ort::ep::CPU::default().build());
150-
return Ok(Session::builder()?
151-
.with_execution_providers(eps)?
152-
.commit_from_file(model_path)?);
153-
}
154-
155-
#[allow(unreachable_code)]
156-
Ok(Session::builder()?.commit_from_file(model_path)?)
157-
}
158-
159130
pub fn run(&self, image_path: &Path) -> Result<TagResult, InferError> {
160131
let tensor = self.preprocess(image_path)?;
161132
let scores = self.infer(tensor)?;

‎yu-hailo-infer-core/src/ep.rs‎

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
//! Execution-provider selection shared by every ONNX session in this crate.
2+
//!
3+
//! Which providers are compiled in is a build-time decision made by cargo
4+
//! features (`cuda`, `rocm`, `coreml`, `directml`, `openvino`). Each one is
5+
//! registered with ORT's default non-fatal behaviour, so a provider whose
6+
//! hardware, driver or dynamic library is absent at runtime is skipped with a
7+
//! warning instead of failing the session. CPU is always appended last, so
8+
//! every build -- including one with no GPU feature at all -- still runs.
9+
10+
use std::path::Path;
11+
12+
use ort::{ep::ExecutionProviderDispatch, session::Session};
13+
14+
use crate::InferError;
15+
16+
/// Build an ORT session that tries every compiled-in GPU provider in order and
17+
/// falls back to CPU.
18+
pub(crate) fn build_session(model_path: &Path) -> Result<Session, InferError> {
19+
let builder = Session::builder()?;
20+
let providers = gpu_providers();
21+
let mut builder = if providers.is_empty() {
22+
// No GPU feature compiled in: ORT's own default list is CPU-only, and
23+
// leaving it alone keeps this identical to the pre-feature behaviour.
24+
builder
25+
} else {
26+
let mut providers = providers;
27+
// `with_execution_providers` replaces the default list rather than
28+
// prepending to it, so CPU has to be named explicitly -- otherwise a
29+
// build whose GPU provider fails to register has nothing left to run.
30+
providers.push(ort::ep::CPU::default().build());
31+
// `with_execution_providers` reports a *recoverable* error, which keeps
32+
// the builder inside the error value and so has a different type from
33+
// the rest of ort's API. Drop the recovery payload to get back to the
34+
// plain `ort::Error` that InferError knows how to absorb.
35+
builder
36+
.with_execution_providers(providers)
37+
.map_err(ort::Error::from)?
38+
};
39+
Ok(builder.commit_from_file(model_path)?)
40+
}
41+
42+
/// The GPU providers this binary was compiled with, in registration order.
43+
///
44+
/// Order is priority: ORT tries each in turn and uses the first that accepts a
45+
/// given node. The features are mutually exclusive in practice (each build
46+
/// targets one accelerator) but nothing here depends on that.
47+
fn gpu_providers() -> Vec<ExecutionProviderDispatch> {
48+
#[allow(unused_mut)]
49+
let mut providers: Vec<ExecutionProviderDispatch> = Vec::new();
50+
51+
#[cfg(feature = "cuda")]
52+
{
53+
tracing::info!("Requesting CUDA execution provider");
54+
providers.push(ort::ep::CUDA::default().build());
55+
}
56+
#[cfg(feature = "rocm")]
57+
{
58+
tracing::info!("Requesting ROCm execution provider");
59+
providers.push(ort::ep::ROCm::default().build());
60+
}
61+
#[cfg(feature = "coreml")]
62+
{
63+
tracing::info!("Requesting CoreML execution provider");
64+
providers.push(ort::ep::CoreML::default().build());
65+
}
66+
#[cfg(feature = "directml")]
67+
{
68+
tracing::info!("Requesting DirectML execution provider");
69+
providers.push(ort::ep::DirectML::default().build());
70+
}
71+
#[cfg(feature = "openvino")]
72+
{
73+
tracing::info!("Requesting OpenVINO execution provider");
74+
providers.push(ort::ep::OpenVINO::default().build());
75+
}
76+
77+
providers
78+
}
79+
80+
#[cfg(test)]
81+
mod tests {
82+
use super::*;
83+
84+
/// The provider list must match the features this build enabled: a `#[cfg]`
85+
/// that stops matching its feature (a rename, a typo, a feature dropped
86+
/// from Cargo.toml) silently returns the binary to CPU-only inference,
87+
/// which is exactly the failure this module exists to end. Counting the
88+
/// features here fails the build instead of degrading it quietly.
89+
#[test]
90+
fn provider_count_matches_enabled_features() {
91+
let expected = usize::from(cfg!(feature = "cuda"))
92+
+ usize::from(cfg!(feature = "rocm"))
93+
+ usize::from(cfg!(feature = "coreml"))
94+
+ usize::from(cfg!(feature = "directml"))
95+
+ usize::from(cfg!(feature = "openvino"));
96+
assert_eq!(gpu_providers().len(), expected);
97+
}
98+
}

‎yu-hailo-infer-core/src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mod clip;
22
pub mod clip_image;
33
pub mod clip_text;
44
pub mod engine;
5+
mod ep;
56
pub mod error;
67
pub mod profile;
78
pub mod tags;

0 commit comments

Comments
 (0)