Skip to content

Commit 6233807

Browse files
ultralytics-inference 0.0.16 fix: 🐞 resolve CoreML graph_input_cast_0 crash and add regression tests (#173)
Signed-off-by: Onuralp SEZER <onuralp@ultralytics.com> Co-authored-by: UltralyticsAssistant <web@ultralytics.com>
1 parent 7138e4c commit 6233807

6 files changed

Lines changed: 127 additions & 26 deletions

File tree

.github/workflows/ci.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,26 @@ jobs:
4242
run: rustup show
4343

4444
- name: Clippy (annotate feature)
45+
if: matrix.os != 'macos-latest'
4546
run: cargo clippy --all-targets --no-default-features --features annotate -- -D warnings
4647

4748
- name: Tests (annotate feature)
49+
if: matrix.os != 'macos-latest'
4850
run: cargo test --no-default-features --features annotate
4951

52+
# CoreML is macOS-only — covers issue #148 (GatherElements warmup) and graph_input_cast_0 fix
53+
- name: Clippy (coreml + annotate)
54+
if: matrix.os == 'macos-latest'
55+
run: cargo clippy --all-targets --no-default-features --features "coreml,annotate" -- -D warnings
56+
57+
- name: Tests (coreml + annotate)
58+
if: matrix.os == 'macos-latest'
59+
run: cargo test --no-default-features --features "coreml,annotate"
60+
61+
- name: Integration tests (CoreML regression, including ignored)
62+
if: matrix.os == 'macos-latest'
63+
run: cargo test --no-default-features --features "coreml,annotate" --test integration_test test_coreml_model_loads_and_warms_up -- --ignored --exact
64+
5065
# Video feature tests for Linux - FFmpeg 7 and 8
5166
test-video:
5267
name: test / linux / ffmpeg ${{ matrix.ffmpeg_version }}

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[package]
44
name = "ultralytics-inference"
5-
version = "0.0.15"
5+
version = "0.0.16"
66
edition = "2024"
77
rust-version = "1.88"
88
authors = [

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ ultralytics-inference predict --model yolo26n.onnx --source image.jpg --rect
135135
136136
WARNING ⚠️ 'model' argument is missing. Using default '--model=yolo26n.onnx'.
137137
WARNING ⚠️ 'source' argument is missing. Using default images: https://ultralytics.com/images/bus.jpg, https://ultralytics.com/images/zidane.jpg
138-
Ultralytics Inference 0.0.15 🚀 Rust ONNX FP32 CPU
138+
Ultralytics Inference 0.0.16 🚀 Rust ONNX FP32 CPU
139139
Using ONNX Runtime CPUExecutionProvider
140140
YOLO26n summary: 80 classes, imgsz=(640, 640)
141141
@@ -153,7 +153,7 @@ Results saved to runs/detect/predict1
153153

154154
WARNING ⚠️ 'model' argument is missing. Using default '--model=yolo26n-seg.onnx'.
155155
WARNING ⚠️ 'source' argument is missing. Using default images: https://ultralytics.com/images/bus.jpg, https://ultralytics.com/images/zidane.jpg
156-
Ultralytics Inference 0.0.15 🚀 Rust ONNX FP32 CPU
156+
Ultralytics Inference 0.0.16 🚀 Rust ONNX FP32 CPU
157157
Using ONNX Runtime CPUExecutionProvider
158158
YOLO26n-seg summary: 80 classes, imgsz=(640, 640)
159159

@@ -243,7 +243,7 @@ Add to your `Cargo.toml` (choose one):
243243
```toml
244244
# Stable release from crates.io
245245
[dependencies]
246-
ultralytics-inference = "0.0.15"
246+
ultralytics-inference = "0.0.16"
247247
```
248248

249249
```toml

src/model.rs

Lines changed: 79 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,12 @@ impl YOLOModel {
155155
}
156156
#[cfg(feature = "coreml")]
157157
crate::Device::CoreMl => {
158-
eps.push(Self::build_coreml_ep(path));
159-
provider_name = "CoreMLExecutionProvider";
158+
if matches!(Self::macos_version(), Some((major, _)) if major >= 11) {
159+
eps.push(Self::build_coreml_ep(path));
160+
provider_name = "CoreMLExecutionProvider";
161+
} else {
162+
warn!("WARNING ⚠️ CoreML requires macOS 11+; falling back to CPU.");
163+
}
160164
}
161165
#[cfg(feature = "tensorrt")]
162166
crate::Device::TensorRt(i) => {
@@ -222,7 +226,7 @@ impl YOLOModel {
222226
}
223227

224228
#[cfg(feature = "coreml")]
225-
{
229+
if matches!(Self::macos_version(), Some((major, _)) if major >= 11) {
226230
eps.push(Self::build_coreml_ep(path));
227231
if provider_name == "CPUExecutionProvider" {
228232
provider_name = "CoreMLExecutionProvider";
@@ -433,19 +437,12 @@ impl YOLOModel {
433437
#[cfg(feature = "coreml")]
434438
fn build_coreml_ep(model_path: &Path) -> ort::execution_providers::ExecutionProviderDispatch {
435439
use ort::execution_providers::coreml::ModelFormat;
436-
let format = match Self::macos_version() {
437-
Some((major, _)) if major >= 12 => ModelFormat::MLProgram,
438-
ver => {
439-
let label = ver.map_or_else(
440-
|| "unknown".to_owned(),
441-
|(maj, min)| format!("{maj}.{min} < 12"),
442-
);
443-
warn!(
444-
"WARNING ⚠️ macOS {label}: CoreML using NeuralNetwork format; FP16 models may fail."
445-
);
446-
ModelFormat::NeuralNetwork
447-
}
448-
};
440+
// `MLProgram` (macOS 12+) causes ORT's `CoreML` EP to insert a FP32→FP16 cast node at
441+
// graph input, renaming it from the `ONNX` name (e.g. "images") to "graph_input_cast_0".
442+
// ORT then feeds the tensor by the original name, which `CoreML` can't find → crash.
443+
// `NeuralNetwork` (`CoreML` 3, macOS 10.15+) avoids the rename and supports FP16 inputs
444+
// natively — confirmed working with both `FP32` and `FP16` `YOLO` `ONNX` models on macOS 12+.
445+
let format = ModelFormat::NeuralNetwork;
449446
let mut ep =
450447
ort::execution_providers::CoreMLExecutionProvider::default().with_model_format(format);
451448

@@ -518,11 +515,7 @@ impl YOLOModel {
518515

519516
if let Err(e) = warmup_result {
520517
let msg = e.to_string();
521-
// CoreML + all-zeros input: GatherElements out-of-range in the DFL head is benign.
522-
if self.execution_provider != "CoreMLExecutionProvider"
523-
|| !msg.contains("GatherElements")
524-
|| !msg.contains("Out of range")
525-
{
518+
if !is_benign_coreml_warmup_error(&self.execution_provider, &msg) {
526519
return Err(e);
527520
}
528521
}
@@ -1161,6 +1154,15 @@ impl std::fmt::Debug for YOLOModel {
11611154
}
11621155
}
11631156

1157+
/// Returns true if `err` is the benign `GatherElements` out-of-range error that `CoreML` produces
1158+
/// on all-zeros dummy input (issue #148). All other errors, including `graph_input_cast_0`,
1159+
/// must propagate so callers see real failures.
1160+
fn is_benign_coreml_warmup_error(provider: &str, msg: &str) -> bool {
1161+
provider == "CoreMLExecutionProvider"
1162+
&& msg.contains("GatherElements")
1163+
&& msg.contains("Out of range")
1164+
}
1165+
11641166
#[cfg(test)]
11651167
mod tests {
11661168
use super::*;
@@ -1212,4 +1214,60 @@ mod tests {
12121214
assert!(debug_str.contains("num_classes"));
12131215
}
12141216
}
1217+
1218+
// Issue #148 , PR #149: GatherElements out-of-range on an all-zeros dummy input is benign
1219+
// during `CoreML` warmup (the DFL head produces invalid gather indices for zero activations).
1220+
#[test]
1221+
fn test_warmup_gather_elements_suppressed_for_coreml() {
1222+
assert!(is_benign_coreml_warmup_error(
1223+
"CoreMLExecutionProvider",
1224+
"GatherElements op: Out of range value in index tensor"
1225+
));
1226+
}
1227+
1228+
// The same GatherElements error on CPU/CUDA is a real bug and must not be hidden.
1229+
#[test]
1230+
fn test_warmup_gather_elements_propagates_for_other_providers() {
1231+
assert!(!is_benign_coreml_warmup_error(
1232+
"CPUExecutionProvider",
1233+
"GatherElements op: Out of range value in index tensor"
1234+
));
1235+
assert!(!is_benign_coreml_warmup_error(
1236+
"CUDAExecutionProvider",
1237+
"GatherElements op: Out of range value in index tensor"
1238+
));
1239+
}
1240+
1241+
// graph_input_cast_0 is a real `CoreML` misconfiguration (MLProgram adds a cast node that
1242+
// renames the ONNX input). It must propagate so the caller sees the failure.
1243+
// The fix (`NeuralNetwork` format) prevents this error from occurring at all, but it must
1244+
// never be silently swallowed if it somehow reappears.
1245+
#[test]
1246+
fn test_warmup_graph_input_cast_error_propagates() {
1247+
assert!(!is_benign_coreml_warmup_error(
1248+
"CoreMLExecutionProvider",
1249+
"Feature graph_input_cast_0 is required but not specified"
1250+
));
1251+
}
1252+
1253+
// Any unrecognized `CoreML` error must propagate.
1254+
#[test]
1255+
fn test_warmup_unrecognised_coreml_error_propagates() {
1256+
assert!(!is_benign_coreml_warmup_error(
1257+
"CoreMLExecutionProvider",
1258+
"Some unexpected `CoreML` error"
1259+
));
1260+
}
1261+
1262+
#[cfg(all(feature = "coreml", target_os = "macos"))]
1263+
#[test]
1264+
fn test_macos_version_parses_on_macos() {
1265+
let version = YOLOModel::macos_version();
1266+
assert!(
1267+
version.is_some(),
1268+
"macos_version() must return Some on macOS"
1269+
);
1270+
let (major, _minor) = version.unwrap();
1271+
assert!(major >= 10, "macOS major version should be >= 10");
1272+
}
12151273
}

tests/integration_test.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,34 @@ use tempfile::tempdir;
1010
use ultralytics_inference::cli::args::PredictArgs;
1111
use ultralytics_inference::cli::predict::run_prediction;
1212
use ultralytics_inference::{Boxes, InferenceConfig, Results, Speed};
13+
#[cfg(feature = "coreml")]
14+
use ultralytics_inference::{Device, YOLOModel};
15+
16+
/// End-to-end `CoreML` test covering two known regressions:
17+
///
18+
/// 1. **Issue #148 / PR #149** — `GatherElements op: Out of range` during warmup.
19+
/// `CoreML`'s DFL head produces out-of-range gather indices on an all-zeros dummy input.
20+
///
21+
///
22+
/// 2. **`graph_input_cast_0` crash** — `MLProgram` format causes ORT's `CoreML` EP to insert a
23+
/// cast node, renaming the ONNX input (e.g. `images`) to `graph_input_cast_0`. ORT then
24+
/// feeds the tensor with the original name, which `CoreML` can't find.
25+
/// The fix (`NeuralNetwork` format) avoids the rename entirely.
26+
#[cfg(feature = "coreml")]
27+
#[test]
28+
#[ignore = "downloads a YOLO model; requires CoreML (macOS only)"]
29+
fn test_coreml_model_loads_and_warms_up() {
30+
let temp_dir = tempdir().expect("temp dir should be created");
31+
let model_path = temp_dir.path().join("yolo26n.onnx");
32+
33+
let config = InferenceConfig::new().with_device(Device::CoreMl);
34+
let mut model = YOLOModel::load_with_config(model_path.to_string_lossy().as_ref(), config)
35+
.expect("CoreML model should load");
36+
37+
// warmup() must succeed: graph_input_cast_0 errors are gone (`NeuralNetwork` format fix)
38+
// and GatherElements out-of-range is tolerated (issue #148 fix).
39+
model.warmup().expect("CoreML warmup should not fail");
40+
}
1341

1442
#[test]
1543
#[ignore = "downloads a YOLO model and sample image"]

0 commit comments

Comments
 (0)