From 776decb623f19ab80abcd69b8029f960202e71b6 Mon Sep 17 00:00:00 2001 From: Maxim Davgalev Date: Mon, 10 Aug 2026 16:30:48 -0700 Subject: [PATCH 01/12] Expose CoreML EP via OrtEpFactory Register an internal CoreMLEpFactory with the plugin-EP infrastructure so that, on supported Apple systems with discovered accelerator hardware, the CoreML EP appears in GetEpDevices(), can be added to a session with SessionOptionsAppendExecutionProvider_V2, and is eligible for EP selection policies, notably PREFER_NPU, which can now select the Apple Neural Engine on supported systems instead of silently falling back to the CPU EP. Fixes #29066 The factory registers with vendor ID OrtDevice::VendorIds::APPLE, matching the vendor ID reported by Apple device discovery, so device ordering during EP selection prefers CoreML for Apple hardware via the vendor-affinity rule. In builds that also register the internal WebGPU EP, including the official macOS packages, PREFER_GPU and MAX_PERFORMANCE now select CoreML instead of WebGPU when both EPs claim the same Apple GPU, because CoreML has matching vendor affinity. The factory advertises at most one NPU and one GPU from device discovery. It advertises no devices below Core ML 5, the EP's minimum supported version. The NPU additionally requires Core ML 6 (iOS 16 / macOS 13), because earlier Core ML versions have no compute-units mode that enables the Neural Engine without also enabling the GPU. MLComputeUnitsCPUAndNeuralEngine requires Core ML 6, so an NPU selection could not otherwise be honored. The CPU device is deliberately left to the CPU EP. CPU-selecting policies choose a single preferred EP for the CPU device, so advertising it would displace the ORT CPU EP and other CPU-based EPs. CoreML-on-CPU remains available through an explicit MLComputeUnits=CPUOnly option, and Core ML can use its internal CPU path for operations unsupported by the selected compute units. When MLComputeUnits is not specified, the factory derives it from the selected devices: CPUAndNeuralEngine for NPU, CPUAndGPU for GPU, and ALL for NPU+GPU. User options may narrow this set, but may not enable an accelerator that was not selected. Tests in onnxruntime/test/autoep/test_selection.cc cover factory registration, explicit GPU and NPU+GPU selection, inference, PREFER_NPU/PREFER_GPU and their policy aliases, compute-unit defaulting and conflict validation, version checks, device validation and selection limits, CPU-only narrowing, and transactional cleanup after partial device-creation failures. --- .../onnxruntime/core/framework/ortdevice.h | 1 + .../coreml/coreml_provider_factory.h | 15 +- .../core/platform/apple/device_discovery.cc | 3 +- .../core/providers/coreml/coreml_options.cc | 8 +- .../core/providers/coreml/model/host_utils.h | 5 + .../session/plugin_ep/ep_factory_coreml.cc | 245 +++++ .../session/plugin_ep/ep_factory_coreml.h | 123 +++ .../session/plugin_ep/ep_library_internal.cc | 15 + .../session/plugin_ep/ep_library_internal.h | 3 + .../onnxruntime_inference_collection.py | 1 + onnxruntime/test/autoep/test_selection.cc | 970 +++++++++++++++++- .../test/python/onnxruntime_test_python.py | 17 + 12 files changed, 1393 insertions(+), 13 deletions(-) create mode 100644 onnxruntime/core/session/plugin_ep/ep_factory_coreml.cc create mode 100644 onnxruntime/core/session/plugin_ep/ep_factory_coreml.h diff --git a/include/onnxruntime/core/framework/ortdevice.h b/include/onnxruntime/core/framework/ortdevice.h index c85b01210fc3b..11e06ff1bc8a9 100644 --- a/include/onnxruntime/core/framework/ortdevice.h +++ b/include/onnxruntime/core/framework/ortdevice.h @@ -57,6 +57,7 @@ struct OrtDevice { // No vendor ID. Valid for DeviceType::CPU + MemType::DEFAULT or for generic allocators like WebGPU. NONE = 0x0000, AMD = 0x1002, // MIGraphX EP + APPLE = 0x106B, // CoreML EP NVIDIA = 0x10DE, // CUDA/TensorRT ARM = 0x13B5, // ARM GPU EP MICROSOFT = 0x1414, // DML EP diff --git a/include/onnxruntime/core/providers/coreml/coreml_provider_factory.h b/include/onnxruntime/core/providers/coreml/coreml_provider_factory.h index 351eafc2a4675..62f57acdb84e2 100644 --- a/include/onnxruntime/core/providers/coreml/coreml_provider_factory.h +++ b/include/onnxruntime/core/providers/coreml/coreml_provider_factory.h @@ -41,10 +41,17 @@ enum COREMLFlags { COREML_FLAG_LAST = COREML_FLAG_USE_CPU_AND_GPU, }; -// MLComputeUnits can be one of the following values: -// 'MLComputeUnitsCPUAndNeuralEngine|MLComputeUnitsCPUAndGPU|MLComputeUnitsCPUOnly|MLComputeUnitsAll' -// these values are intended to be used with Ort::SessionOptions::AppendExecutionProvider (C++ API) -// and SessionOptionsAppendExecutionProvider (C API). For the old API, use COREMLFlags instead. +// Set the MLComputeUnits provider option to one of the kCoremlComputeUnits_* values below. Each value maps to +// the corresponding Core ML compute-unit mode +// (https://developer.apple.com/documentation/coreml/mlcomputeunits). +// Use these values with SessionOptionsAppendExecutionProvider or SessionOptionsAppendExecutionProvider_V2. +// The legacy CoreML-specific OrtSessionOptionsAppendExecutionProvider_CoreML API uses a COREMLFlags bitmask +// instead. +// MLComputeUnitsCPUAndNeuralEngine requires Core ML 6 (iOS 16 / macOS 13) and is unavailable on earlier systems. +static const char* const kCoremlComputeUnits_CPUAndNeuralEngine = "CPUAndNeuralEngine"; +static const char* const kCoremlComputeUnits_CPUAndGPU = "CPUAndGPU"; +static const char* const kCoremlComputeUnits_CPUOnly = "CPUOnly"; +static const char* const kCoremlComputeUnits_ALL = "ALL"; static const char* const kCoremlProviderOption_MLComputeUnits = "MLComputeUnits"; static const char* const kCoremlProviderOption_ModelFormat = "ModelFormat"; // same as COREML_FLAG_ONLY_ALLOW_STATIC_INPUT_SHAPES diff --git a/onnxruntime/core/platform/apple/device_discovery.cc b/onnxruntime/core/platform/apple/device_discovery.cc index 767b834e38756..addaa419f7ef7 100644 --- a/onnxruntime/core/platform/apple/device_discovery.cc +++ b/onnxruntime/core/platform/apple/device_discovery.cc @@ -7,12 +7,13 @@ #include #include "core/common/logging/logging.h" +#include "core/framework/ortdevice.h" namespace onnxruntime { namespace { -constexpr auto kApplePciVendorId = 0x106B; +constexpr auto kApplePciVendorId = OrtDevice::VendorIds::APPLE; constexpr auto kAppleVendorName = "Apple"; std::vector GetGpuDevices() { diff --git a/onnxruntime/core/providers/coreml/coreml_options.cc b/onnxruntime/core/providers/coreml/coreml_options.cc index c441a2eff56e0..f3adf15dafd1f 100644 --- a/onnxruntime/core/providers/coreml/coreml_options.cc +++ b/onnxruntime/core/providers/coreml/coreml_options.cc @@ -43,10 +43,10 @@ CoreMLOptions::CoreMLOptions(uint32_t coreml_flags) { void CoreMLOptions::ValidateAndParseProviderOption(const ProviderOptions& options) { const std::unordered_map available_computeunits_options = { - {"CPUAndNeuralEngine", COREML_FLAG_ONLY_ENABLE_DEVICE_WITH_ANE}, - {"CPUAndGPU", COREML_FLAG_USE_CPU_AND_GPU}, - {"CPUOnly", COREML_FLAG_USE_CPU_ONLY}, - {"ALL", COREML_FLAG_USE_NONE}, + {kCoremlComputeUnits_CPUAndNeuralEngine, COREML_FLAG_ONLY_ENABLE_DEVICE_WITH_ANE}, + {kCoremlComputeUnits_CPUAndGPU, COREML_FLAG_USE_CPU_AND_GPU}, + {kCoremlComputeUnits_CPUOnly, COREML_FLAG_USE_CPU_ONLY}, + {kCoremlComputeUnits_ALL, COREML_FLAG_USE_NONE}, }; const std::unordered_map available_modelformat_options = { {"MLProgram", COREML_FLAG_CREATE_MLPROGRAM}, diff --git a/onnxruntime/core/providers/coreml/model/host_utils.h b/onnxruntime/core/providers/coreml/model/host_utils.h index 92eda24e5c46d..824d2d13ac7cc 100644 --- a/onnxruntime/core/providers/coreml/model/host_utils.h +++ b/onnxruntime/core/providers/coreml/model/host_utils.h @@ -62,6 +62,11 @@ #define MINIMUM_COREML_VERSION 5 // first version we support +// Core ML 6 (iOS 16 / macOS 13) introduced MLComputeUnitsCPUAndNeuralEngine, the only compute-units mode that +// enables the Neural Engine without also enabling the GPU. Earlier versions cannot honor a Neural-Engine-only +// request, so device-based EP selection does not offer the Neural Engine below this version. +#define MINIMUM_COREML_VERSION_FOR_NEURAL_ENGINE_SELECTION 6 + namespace onnxruntime { namespace coreml { namespace util { diff --git a/onnxruntime/core/session/plugin_ep/ep_factory_coreml.cc b/onnxruntime/core/session/plugin_ep/ep_factory_coreml.cc new file mode 100644 index 0000000000000..4291c876eec63 --- /dev/null +++ b/onnxruntime/core/session/plugin_ep/ep_factory_coreml.cc @@ -0,0 +1,245 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// CoreML can be built on non-Apple platforms to test model conversion. Such builds report Core ML 7 but cannot +// execute models, even when device discovery finds GPUs. Require __APPLE__ so the factory cannot advertise those +// GPUs. On Apple platforms, the factory accepts any discovered GPU regardless of vendor. GPU discovery currently +// supports only Apple Silicon Macs. Support for iOS and Intel Macs remains TODO in +// core/platform/apple/device_discovery.cc. +#if defined(USE_COREML) && defined(__APPLE__) + +#include "core/session/plugin_ep/ep_factory_coreml.h" + +#include "core/common/common.h" +#include "core/framework/error_code_helper.h" +#include "core/providers/coreml/coreml_provider_factory.h" +#include "core/providers/coreml/coreml_provider_factory_creator.h" +#include "core/providers/coreml/model/host_utils.h" +#include "core/session/abi_devices.h" +#include "core/session/abi_logger.h" +#include "core/session/abi_session_options_impl.h" +#include "core/session/plugin_ep/ep_api.h" +#include "core/session/plugin_ep/ep_factory_internal.h" +#include "core/session/ort_apis.h" + +namespace onnxruntime { + +bool CoreMLEpFactory::CanClaimDeviceType(OrtHardwareDeviceType device_type, int32_t coreml_version) { + // The factory advertises no devices below Core ML 5 (macOS 12 / iOS 15), the minimum version supported by + // the current CoreML EP. Although earlier Core ML versions can use accelerators, CoreMLExecutionProvider + // rejects those systems during construction. Advertising a device on those systems would cause session creation + // to fail after selection instead of allowing the policy to select another EP. + if (coreml_version < MINIMUM_COREML_VERSION) { + return false; + } + + switch (device_type) { + case OrtHardwareDeviceType::OrtHardwareDeviceType_GPU: + return true; + + case OrtHardwareDeviceType::OrtHardwareDeviceType_NPU: + // The NPU is claimed only on Core ML 6 or later (iOS 16 / macOS 13): earlier Core ML has no MLComputeUnits + // mode that enables the Neural Engine without also enabling the GPU (CPUAndNeuralEngine requires Core ML 6), + // so an NPU selection could not be honored. Core ML can still use the ANE through ALL on those systems. + // The factory therefore does not advertise the NPU on Core ML 5 systems: iOS 15 devices with an ANE and + // Apple Silicon Macs running macOS 12. + return coreml_version >= MINIMUM_COREML_VERSION_FOR_NEURAL_ENGINE_SELECTION; + + default: + // The CPU device is deliberately not claimed. DEFAULT and PREFER_CPU select a single preferred EP for the CPU + // device. The NPU- and GPU-preferring policies first select an EP for the accelerator and then use the same CPU + // selection as a fallback. + // If CoreML claimed the CPU, vendor affinity on Apple hardware would make it the preferred EP for that device, + // ahead of the ORT CPU EP and other CPU-based EPs. + // CoreML-on-CPU remains available through an explicit MLComputeUnits=CPUOnly option. Core ML may also use its + // internal CPU path for operations unsupported by the selected compute units. + return false; + } +} + +InlinedVector CoreMLEpFactory::SelectDevicesToClaim( + gsl::span devices, int32_t coreml_version, size_t max_ep_devices) { + InlinedVector selected; + bool npu_claimed = false; + bool gpu_claimed = false; + + for (size_t i = 0; i < devices.size() && selected.size() < max_ep_devices; ++i) { + const OrtHardwareDevice* device = devices[i]; + + // CanClaimDeviceType checks the Core ML version and excludes CPU devices. + if (!CanClaimDeviceType(device->type, coreml_version)) { + continue; + } + + // Core ML selects compute-unit classes, not individual physical devices, so advertise at most one NPU and one GPU. + // Apple device discovery currently reports at most one device of each type. If it later reports several devices + // of the same type, this function keeps the first one. Additional devices are intentionally ignored because + // Core ML cannot target a specific physical device. + // CanClaimDeviceType currently accepts only NPU and GPU devices, so this logic treats every accepted non-NPU + // device as a GPU. If support for another device type is added, update this selection logic as well. + const bool is_npu = device->type == OrtHardwareDeviceType::OrtHardwareDeviceType_NPU; + bool& claimed = is_npu ? npu_claimed : gpu_claimed; + + if (claimed) { + continue; + } + + claimed = true; + + selected.push_back(device); + } + + return selected; +} + +OrtStatus* CoreMLEpFactory::GetSupportedDevices(EpFactoryInternal& ep_factory, + const OrtHardwareDevice* const* devices, + size_t num_devices, + OrtEpDevice** ep_devices, + size_t max_ep_devices, + size_t* p_num_ep_devices) noexcept { + OrtStatus* status = nullptr; + + ORT_TRY { + const auto selected = + SelectDevicesToClaim(gsl::make_span(devices, num_devices), coreml::util::CoreMLVersion(), max_ep_devices); + + // Do not attach MLComputeUnits to individual OrtEpDevice instances. Per-device defaults would assign different + // values to the NPU and GPU, creating a conflict when both are selected. CreateIExecutionProvider instead computes + // a single value from all selected devices. Any MLComputeUnits value already present in the session options was + // provided by the caller and is preserved if it is compatible with the selection. + const auto create_ep_device = + [&ep_factory](const OrtHardwareDevice& device, OrtEpDevice** ep_device) -> OrtStatus* { + return OrtExecutionProviderApi::CreateEpDevice(&ep_factory, &device, nullptr, nullptr, ep_device); + }; + + const auto release_ep_device = [](OrtEpDevice* ep_device) { + OrtExecutionProviderApi::ReleaseEpDevice(ep_device); + }; + + status = CreateAndPublishEpDevices( + selected, + create_ep_device, + release_ep_device, + ep_devices, p_num_ep_devices); + } + ORT_CATCH(const std::exception& ex) { + // Convert local allocation failures to OrtStatus because this method is noexcept. Device selection and storage + // reservation finish before CreateEpDevice is called, so this exception path cannot leak an OrtEpDevice. + ORT_HANDLE_EXCEPTION([&]() { + status = OrtApis::CreateStatus(ORT_FAIL, ex.what()); + }); + } + + return status; +} + +OrtStatus* CoreMLEpFactory::CreateIExecutionProvider(const OrtHardwareDevice* const* devices, + const OrtKeyValuePairs* const* /*ep_metadata_pairs*/, + size_t num_devices, + const OrtSessionOptions* session_options, + const OrtLogger* session_logger, + std::unique_ptr* ep) noexcept { + OrtStatus* status = nullptr; + *ep = nullptr; + + ORT_TRY { + ProviderOptions options = GetOptionsFromSessionOptions(session_options->value); + + // OrtEpDevice instances carry no MLComputeUnits default, so any existing session option can only come from + // the caller. Validate the device selection, derive MLComputeUnits when absent, and reject recognized values + // that enable an unselected accelerator. + // CoreMLOptions performs the remaining option validation during provider creation. + ORT_API_RETURN_IF_ERROR(ValidateDeviceSelectionAndResolveMLComputeUnits(devices, num_devices, options)); + + const auto provider_factory = CoreMLProviderFactoryCreator::Create(options); + *ep = provider_factory->CreateProvider(); + (*ep)->SetLogger(session_logger->ToInternal()); + } + ORT_CATCH(const std::exception& ex) { + ORT_HANDLE_EXCEPTION([&]() { + status = OrtApis::CreateStatus(ORT_FAIL, ex.what()); + }); + } + + return status; +} + +OrtStatus* CoreMLEpFactory::ValidateDeviceSelectionAndResolveMLComputeUnits( + const OrtHardwareDevice* const* devices, size_t num_devices, ProviderOptions& options) { + if (num_devices == 0) { + return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, + "CoreML EP factory requires at least one device to be selected."); + } + + if (devices == nullptr) { + return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, "CoreML device list is null."); + } + + size_t num_npu = 0; + size_t num_gpu = 0; + for (size_t i = 0; i < num_devices; ++i) { + if (devices[i] == nullptr) { + return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, "CoreML device is null."); + } + + switch (devices[i]->type) { + case OrtHardwareDeviceType::OrtHardwareDeviceType_NPU: + ++num_npu; + break; + case OrtHardwareDeviceType::OrtHardwareDeviceType_GPU: + ++num_gpu; + break; + default: + return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, "CoreML EP factory only supports NPU and GPU devices."); + } + } + + // Reject multiple devices of the same type. Core ML's MLComputeUnits selects a device class, not a specific + // physical device, so the CoreML EP cannot honor a selection containing multiple NPUs or GPUs. + // With current device discovery, such entries can only be duplicates. Future discovery could expose multiple + // physical devices of one type. + if (num_npu > 1 || num_gpu > 1) { + return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, + "CoreML EP factory supports one NPU, one GPU, or one NPU plus one GPU. " + "At most one device of each type can be selected."); + } + + // This helper does not recheck the Core ML version. CreateIExecutionProvider receives only devices previously + // advertised by GetSupportedDevices, which uses CanClaimDeviceType to enforce the version requirements. + // Therefore, a selected NPU implies Core ML 6 or later, so CPUAndNeuralEngine is available. + // MLComputeUnits is the only option derived from the selection. ModelFormat is not device-specific and changes + // only when the caller explicitly sets it. + const auto compute_units_it = options.find(kCoremlProviderOption_MLComputeUnits); + if (compute_units_it == options.end()) { + options[kCoremlProviderOption_MLComputeUnits] = + (num_npu > 0 && num_gpu > 0) ? kCoremlComputeUnits_ALL + : num_npu > 0 ? kCoremlComputeUnits_CPUAndNeuralEngine + : kCoremlComputeUnits_CPUAndGPU; + return nullptr; + } + + // A user-provided MLComputeUnits value may disable selected accelerators (e.g. CPUOnly), but it must not enable + // an unselected accelerator. Otherwise, PREFER_NPU could be silently redirected to the GPU. This check only + // enforces compatibility with the selected devices. CoreMLOptions::ValidateAndParseProviderOption later validates + // the option value itself. If that method is extended to accept a new value that enables an accelerator, update + // the classification below accordingly. + const std::string& compute_units = compute_units_it->second; + const bool enables_npu = + compute_units == kCoremlComputeUnits_CPUAndNeuralEngine || compute_units == kCoremlComputeUnits_ALL; + const bool enables_gpu = + compute_units == kCoremlComputeUnits_CPUAndGPU || compute_units == kCoremlComputeUnits_ALL; + + if ((enables_npu && num_npu == 0) || (enables_gpu && num_gpu == 0)) { + const std::string message = MakeString("MLComputeUnits value '", compute_units, + "' enables a device type that was not selected for the CoreML EP."); + + return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, message.c_str()); + } + + return nullptr; +} + +} // namespace onnxruntime + +#endif // defined(USE_COREML) && defined(__APPLE__) diff --git a/onnxruntime/core/session/plugin_ep/ep_factory_coreml.h b/onnxruntime/core/session/plugin_ep/ep_factory_coreml.h new file mode 100644 index 0000000000000..a743a3a8ffdb4 --- /dev/null +++ b/onnxruntime/core/session/plugin_ep/ep_factory_coreml.h @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +// CoreML can be built on non-Apple platforms to test model conversion. Such builds report Core ML 7 but cannot +// execute models, even when device discovery finds GPUs. Require __APPLE__ so the factory cannot advertise those +// GPUs. On Apple platforms, the factory accepts any discovered GPU regardless of vendor. GPU discovery currently +// supports only Apple Silicon Macs. Support for iOS and Intel Macs remains TODO in +// core/platform/apple/device_discovery.cc. +#if defined(USE_COREML) && defined(__APPLE__) + +#include "core/session/plugin_ep/ep_factory_internal_impl.h" + +#include + +#include + +#include "core/common/common.h" +#include "core/common/inlined_containers.h" +#include "core/framework/ortdevice.h" +#include "core/framework/provider_options.h" +#include "core/graph/constants.h" + +namespace onnxruntime { + +class CoreMLEpFactory : public EpFactoryInternalImpl { + public: + // Report "Apple" and VendorIds::APPLE as the factory vendor identifiers so device ordering gives CoreML + // affinity for Apple hardware. The vendor ID is checked first, with the vendor name used as a fallback. + // Provider-bridge factories copy vendor identifiers from the OrtEpFactory they wrap. + // The CPU, DML, and WebGPU internal factories report "Microsoft". CoreML reports "Apple" through + // EpDevice_EpVendor. + // When multiple EPs target the same NPU or GPU and have equal vendor affinity, they are ordered by EP name. + CoreMLEpFactory() : EpFactoryInternalImpl(kCoreMLExecutionProvider, "Apple", OrtDevice::VendorIds::APPLE) {} + + ORT_DISALLOW_COPY_ASSIGNMENT_AND_MOVE(CoreMLEpFactory); + + // Validates that 'devices' contains exactly one NPU, one GPU, or one NPU plus one GPU. Otherwise, returns + // ORT_INVALID_ARGUMENT. The CPU device is never part of a valid selection as the factory does not claim it. + // If 'options' does not contain MLComputeUnits, derives the default from the selection: + // CPUAndNeuralEngine for NPU, CPUAndGPU for GPU, and ALL for NPU plus GPU. + // A caller-provided value may disable selected accelerators, as CPUOnly does, but it must not enable + // an accelerator that was not selected. + // Unrecognized values are left for CoreMLOptions to validate during provider creation. + static OrtStatus* ValidateDeviceSelectionAndResolveMLComputeUnits(const OrtHardwareDevice* const* devices, + size_t num_devices, + ProviderOptions& options); + + // Returns whether the factory advertises an OrtEpDevice for a hardware device of this type at this Core ML + // version. No devices are advertised below Core ML 5 (MINIMUM_COREML_VERSION, the EP's minimum). NPU devices + // additionally require MINIMUM_COREML_VERSION_FOR_NEURAL_ENGINE_SELECTION. CPU devices are never claimed. + static bool CanClaimDeviceType(OrtHardwareDeviceType device_type, int32_t coreml_version); + + // Selects the hardware devices for the factory to advertise. The result contains only devices accepted by + // CanClaimDeviceType, at most one device of each type, and no more than max_ep_devices in total. + // Core ML's MLComputeUnits selects a device class, not a specific physical device, so the CoreML EP cannot + // honor selection of a particular GPU or NPU when multiple devices of that type exist. Apple device discovery + // currently reports at most one device of each type. Additional same-type devices are intentionally ignored. + static InlinedVector SelectDevicesToClaim( + gsl::span devices, int32_t coreml_version, size_t max_ep_devices); + + // Creates one OrtEpDevice for each selected hardware device. The results are published only after all creations + // succeed. If a creation fails, the function releases all devices created by earlier calls and leaves ep_devices + // and p_num_ep_devices unchanged, following ORT's C API convention for failed calls. A successful call always + // writes the count, including zero when no devices were selected. + // GetSupportedDevices supplies callbacks for CreateEpDevice and ReleaseEpDevice. On success, create_ep_device must + // return nullptr and store a valid OrtEpDevice pointer in its output parameter. On failure, it must return an error + // status without modifying the output parameter. + // Neither callback may throw: create_ep_device must report failures through its returned status, and + // release_ep_device must complete cleanup for every device passed to it. + // selected.size() must not exceed the number of entries available in ep_devices. GetSupportedDevices ensures this + // by limiting SelectDevicesToClaim to max_ep_devices. + template + static OrtStatus* CreateAndPublishEpDevices(gsl::span selected, + const CreateEpDeviceFn& create_ep_device, + const ReleaseEpDeviceFn& release_ep_device, + OrtEpDevice** ep_devices, + size_t* p_num_ep_devices) { + InlinedVector created; + // Reserve before creating any devices. When DISABLE_ABSEIL maps InlinedVector to std::vector, this ensures that + // push_back cannot allocate after a device has been created. + created.reserve(selected.size()); + + for (const OrtHardwareDevice* device : selected) { + OrtEpDevice* ep_device = nullptr; + OrtStatus* status = create_ep_device(*device, &ep_device); + if (status != nullptr) { + for (OrtEpDevice* created_device : created) { + release_ep_device(created_device); + } + + return status; + } + + created.push_back(ep_device); + } + + std::copy(created.begin(), created.end(), ep_devices); + *p_num_ep_devices = created.size(); + + return nullptr; + } + + private: + OrtStatus* GetSupportedDevices(EpFactoryInternal& ep_factory, + const OrtHardwareDevice* const* devices, + size_t num_devices, + OrtEpDevice** ep_devices, + size_t max_ep_devices, + size_t* p_num_ep_devices) noexcept override; + + OrtStatus* CreateIExecutionProvider(const OrtHardwareDevice* const* devices, + const OrtKeyValuePairs* const* ep_metadata_pairs, + size_t num_devices, + const OrtSessionOptions* session_options, + const OrtLogger* session_logger, + std::unique_ptr* ep) noexcept override; +}; + +} // namespace onnxruntime + +#endif // defined(USE_COREML) && defined(__APPLE__) diff --git a/onnxruntime/core/session/plugin_ep/ep_library_internal.cc b/onnxruntime/core/session/plugin_ep/ep_library_internal.cc index 0f8913a01188a..ca852b367adcf 100644 --- a/onnxruntime/core/session/plugin_ep/ep_library_internal.cc +++ b/onnxruntime/core/session/plugin_ep/ep_library_internal.cc @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "core/session/plugin_ep/ep_library_internal.h" +#include "core/session/plugin_ep/ep_factory_coreml.h" #include "core/session/plugin_ep/ep_factory_cpu.h" #include "core/session/plugin_ep/ep_factory_dml.h" #include "core/session/plugin_ep/ep_factory_webgpu.h" @@ -14,6 +15,14 @@ std::unique_ptr EpLibraryInternal::CreateCpuEp() { return std::make_unique(std::move(internal_factory)); } +#if defined(USE_COREML) && defined(__APPLE__) +std::unique_ptr EpLibraryInternal::CreateCoreMLEp() { + auto coreml_factory_impl = std::make_unique(); + auto internal_factory = std::make_unique(std::move(coreml_factory_impl)); + return std::make_unique(std::move(internal_factory)); +} +#endif + #if defined(USE_DML) std::unique_ptr EpLibraryInternal::CreateDmlEp() { @@ -48,6 +57,12 @@ std::vector> EpLibraryInternal::CreateInterna internal_eps.push_back(CreateDmlEp()); #endif +// Unlike WebGPU, CoreML has no plugin-EP adapter, so it does not need an ORT_USE_EP_API_ADAPTERS exclusion. +// This matches DML. +#if defined(USE_COREML) && defined(__APPLE__) + internal_eps.push_back(CreateCoreMLEp()); +#endif + return internal_eps; } diff --git a/onnxruntime/core/session/plugin_ep/ep_library_internal.h b/onnxruntime/core/session/plugin_ep/ep_library_internal.h index 352dba9928021..8d246e5539cbb 100644 --- a/onnxruntime/core/session/plugin_ep/ep_library_internal.h +++ b/onnxruntime/core/session/plugin_ep/ep_library_internal.h @@ -45,6 +45,9 @@ class EpLibraryInternal : public EpLibrary { private: static std::unique_ptr CreateCpuEp(); +#if defined(USE_COREML) && defined(__APPLE__) + static std::unique_ptr CreateCoreMLEp(); +#endif #if defined(USE_DML) static std::unique_ptr CreateDmlEp(); #endif diff --git a/onnxruntime/python/onnxruntime_inference_collection.py b/onnxruntime/python/onnxruntime_inference_collection.py index 70f699768e92c..6cbf681cb26bc 100644 --- a/onnxruntime/python/onnxruntime_inference_collection.py +++ b/onnxruntime/python/onnxruntime_inference_collection.py @@ -45,6 +45,7 @@ class OrtDeviceVendorId(IntEnum): NONE = 0x0000 AMD = 0x1002 + APPLE = 0x106B NVIDIA = 0x10DE ARM = 0x13B5 MICROSOFT = 0x1414 diff --git a/onnxruntime/test/autoep/test_selection.cc b/onnxruntime/test/autoep/test_selection.cc index ebbbc40d46fa9..106753c3f744d 100644 --- a/onnxruntime/test/autoep/test_selection.cc +++ b/onnxruntime/test/autoep/test_selection.cc @@ -1,16 +1,32 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +#include #include +#include +#include // #include #include #include +#include "core/framework/provider_options.h" #include "core/graph/constants.h" +#include "core/session/abi_devices.h" #include "core/session/abi_key_value_pairs.h" #include "core/session/abi_session_options_impl.h" #include "core/session/onnxruntime_cxx_api.h" #include "core/session/onnxruntime_ep_device_ep_metadata_keys.h" +#include "core/session/onnxruntime_session_options_config_keys.h" + +#if defined(USE_COREML) && defined(__APPLE__) +#include + +#include "core/common/inlined_containers.h" +#include "core/providers/coreml/coreml_provider_factory.h" +#include "core/providers/coreml/model/host_utils.h" +#include "core/session/plugin_ep/ep_factory_coreml.h" +#include "core/session/provider_policy_context.h" +#endif #include "test_allocator.h" #include "test/autoep/test_autoep_utils.h" @@ -74,9 +90,21 @@ static void TestInference(Ort::Env& env, const std::basic_string& mod // auto select using policy std::optional policy = std::nullopt, std::optional delegate = std::nullopt, - bool test_session_creation_only = false) { + bool test_session_creation_only = false, + // If true, disables fallback of unsupported graph nodes to the ORT CPU + // EP. Session creation fails unless the selected non-CPU EP supports the + // entire graph. Setting this to true while explicitly selecting the ORT + // CPU EP is invalid and causes session creation to fail. + bool disable_cpu_ep_fallback = false, + // Optional callback invoked after session creation and before inference, + // for example to verify the session's EP assignment. + const std::function& session_checker = nullptr) { Ort::SessionOptions session_options; + if (disable_cpu_ep_fallback) { + session_options.AddConfigEntry(kOrtSessionOptionsDisableCPUEPFallback, "1"); + } + if (library_path && IsRegistered(ep_to_select) == false) { ASSERT_ORTSTATUS_OK(Ort::GetApi().RegisterExecutionProviderLibrary(env, ep_to_select.c_str(), library_path->c_str())); @@ -125,6 +153,11 @@ static void TestInference(Ort::Env& env, const std::basic_string& mod // if session creation passes, model loads fine Ort::Session session(env, model_uri.c_str(), session_options); + if (session_checker) { + // Stop this helper if session_checker reports a fatal assertion, rather than continuing to RunSession. + ASSERT_NO_FATAL_FAILURE(session_checker(session)); + } + // caller wants to test running the model (not just loading the model) if (!test_session_creation_only) { auto default_allocator = std::make_unique(); @@ -141,7 +174,16 @@ static void TestInference(Ort::Env& env, const std::basic_string& mod namespace { void RunBasicTest(const std::string& ep_name, std::optional library_path, const Ort::KeyValuePairs& provider_options = Ort::KeyValuePairs{}, - const std::function&)>& select_devices = nullptr) { + const std::function&)>& select_devices = nullptr, + bool test_auto_select = true, + // Optional callback for the session created through AppendExecutionProvider_V2. + const std::function& v2_session_checker = nullptr, + // Applied to both paths. In the AppendExecutionProvider_V2 path, v2_session_checker verifies the + // input assignment directly, while disabling CPU fallback requires the selected EP to handle the + // entire graph. The "test.ep_to_select" path has no session checker, so disabling CPU fallback + // prevents a false pass if any part of the graph is assigned to the ORT CPU EP instead of the + // selected EP. + bool disable_cpu_ep_fallback = false) { const auto run_test = [&](bool auto_select) { std::vector> inputs(1); auto& input = inputs.back(); @@ -160,10 +202,18 @@ void RunBasicTest(const std::string& ep_name, std::optionalGetNumHardwareDevices(*ort_env, &num_devices)); + if (num_devices == 0) { + return found; + } + + InlinedVector devices(num_devices); + Ort::ThrowOnError(c_api->GetHardwareDevices(*ort_env, devices.data(), num_devices)); + + for (const OrtHardwareDevice* device : devices) { + const OrtHardwareDeviceType type = c_api->HardwareDevice_Type(device); + found.has_npu |= type == OrtHardwareDeviceType_NPU; + found.has_gpu |= type == OrtHardwareDeviceType_GPU; + } + + return found; +} + +CoreMLDevices GetCoreMLEpDevices() { + CoreMLDevices found; + size_t num_npu = 0; + size_t num_gpu = 0; + const OrtApi* c_api = &Ort::GetApi(); + const OrtEpDevice* const* ep_devices = nullptr; + size_t num_devices = 0; + + Ort::ThrowOnError(c_api->GetEpDevices(*ort_env, &ep_devices, &num_devices)); + + for (size_t i = 0; i < num_devices; ++i) { + const OrtEpDevice* ep_device = ep_devices[i]; + if (strcmp(c_api->EpDevice_EpName(ep_device), kCoreMLExecutionProvider) != 0) { + continue; + } + + const OrtHardwareDeviceType type = c_api->HardwareDevice_Type(c_api->EpDevice_Device(ep_device)); + if (type == OrtHardwareDeviceType_NPU) { + ++num_npu; + if (found.npu == nullptr) { + found.npu = ep_device; + } + } else if (type == OrtHardwareDeviceType_GPU) { + ++num_gpu; + if (found.gpu == nullptr) { + found.gpu = ep_device; + } + } else { + ADD_FAILURE() << "CoreML EP advertised an OrtEpDevice with unexpected hardware device type " << type; + } + } + + // The factory may advertise at most one NPU and one GPU. This helper retains only the first OrtEpDevice of each + // type, so the separate counters ensure that any additional OrtEpDevice of the same type is detected. + EXPECT_LE(num_npu, 1u) << "CoreML EP advertised more than one NPU OrtEpDevice."; + EXPECT_LE(num_gpu, 1u) << "CoreML EP advertised more than one GPU OrtEpDevice."; + + return found; +} + +// Finds the first CoreML OrtEpDevice returned by GetEpDevices, or nullptr if none is advertised. +const OrtEpDevice* GetFirstCoreMLEpDevice() { + const OrtApi* c_api = &Ort::GetApi(); + const OrtEpDevice* const* ep_devices = nullptr; + size_t num_devices = 0; + + Ort::ThrowOnError(c_api->GetEpDevices(*ort_env, &ep_devices, &num_devices)); + + for (size_t i = 0; i < num_devices; ++i) { + if (strcmp(c_api->EpDevice_EpName(ep_devices[i]), kCoreMLExecutionProvider) == 0) { + return ep_devices[i]; + } + } + + return nullptr; +} + +// Returns whether this machine has an NPU that the CoreML EP factory may advertise at the runtime Core ML version. +bool CoreMLCanClaimNpu(const HardwareDeviceTypes& hardware) { + return hardware.has_npu && + CoreMLEpFactory::CanClaimDeviceType(OrtHardwareDeviceType_NPU, coreml::util::CoreMLVersion()); +} + +// Returns whether this machine has a GPU that the CoreML EP factory may advertise at the runtime Core ML version. +bool CoreMLCanClaimGpu(const HardwareDeviceTypes& hardware) { + return hardware.has_gpu && + CoreMLEpFactory::CanClaimDeviceType(OrtHardwareDeviceType_GPU, coreml::util::CoreMLVersion()); +} + +// GetErrorCode can be called only on a failed Ort::Status. An OK status contains a null OrtStatus*. Verify failure +// first so the helper stops before GetErrorCode if the call unexpectedly succeeds. +// SCOPED_TRACE includes 'selection' in failure messages, making it clear which input failed. +void ExpectInvalidArgument(const Ort::Status& status, std::string_view selection) { + SCOPED_TRACE(selection); + ASSERT_FALSE(status.IsOK()) << "Expected ORT_INVALID_ARGUMENT, but the call succeeded."; + EXPECT_EQ(status.GetErrorCode(), ORT_INVALID_ARGUMENT); +} + +// Verifies that a session for mul_1.onnx assigns its input to the CoreML EP and produces the expected output. +void AssertMul1SessionRunsOnCoreML(Ort::Session& session) { + const OrtApi* c_api = &Ort::GetApi(); + const OrtEpDevice* input_ep_device = nullptr; + ASSERT_ORTSTATUS_OK(c_api->SessionGetEpDeviceForInputs(session, &input_ep_device, 1)); + ASSERT_NE(input_ep_device, nullptr); + ASSERT_STREQ(c_api->EpDevice_EpName(input_ep_device), kCoreMLExecutionProvider); + + std::vector> inputs(1); + auto& input = inputs.back(); + input.name = "X"; + input.dims = {3, 2}; + input.values = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f}; + + auto allocator = std::make_unique(); + RunSession(allocator.get(), session, inputs, "Y", + {3, 2}, {1.0f, 4.0f, 9.0f, 16.0f, 25.0f, 36.0f}, nullptr); +} +} // namespace + +TEST(AutoEpSelection, CoreMLEP) { + const HardwareDeviceTypes hardware = GetHardwareDeviceTypes(); + const bool can_claim_npu = CoreMLCanClaimNpu(hardware); + const bool can_claim_gpu = CoreMLCanClaimGpu(hardware); + const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); + + // Verify that the factory does not advertise devices rejected by its version requirements. + if (hardware.has_npu && !can_claim_npu) { + EXPECT_EQ(coreml_devices.npu, nullptr) << "CoreML EP must not advertise the NPU below Core ML 6."; + } + if (hardware.has_gpu && !can_claim_gpu) { + EXPECT_EQ(coreml_devices.gpu, nullptr) << "CoreML EP must not advertise devices below Core ML 5."; + } + + // Skip when the factory cannot advertise any discovered accelerator. For example, on iOS versions earlier + // than 16, discovery may report an NPU that cannot be advertised because NPU selection requires Core ML 6, + // while GPU discovery does not currently support iOS. + if (!can_claim_npu && !can_claim_gpu) { + GTEST_SKIP() << "No hardware device that the CoreML EP factory can advertise on this machine."; + } + + if (can_claim_npu) { + ASSERT_NE(coreml_devices.npu, nullptr) << "CoreML EP did not claim the NPU hardware device."; + } + if (can_claim_gpu) { + ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not claim the GPU hardware device."; + } + + // Verify the CoreML factory's vendor name and vendor ID, along with the hardware vendor ID. Device ordering + // checks vendor IDs first and uses vendor names as a fallback. + // Apple device discovery currently assigns VendorIds::APPLE to every NPU and GPU it reports. The CoreML factory + // intentionally accepts any discovered GPU regardless of vendor, so this hardware vendor ID assertion must be + // updated if discovery later reports a non-Apple GPU (see the multi-GPU TODO in + // core/platform/apple/device_discovery.cc). + const OrtApi* c_api = &Ort::GetApi(); + size_t num_ep_devices = 0; + const OrtEpDevice* const* all_ep_devices = nullptr; + ASSERT_ORTSTATUS_OK(c_api->GetEpDevices(*ort_env, &all_ep_devices, &num_ep_devices)); + + for (size_t i = 0; i < num_ep_devices; ++i) { + const OrtEpDevice* ep_device = all_ep_devices[i]; + if (strcmp(c_api->EpDevice_EpName(ep_device), kCoreMLExecutionProvider) != 0) { + continue; + } + + EXPECT_STREQ(c_api->EpDevice_EpVendor(ep_device), "Apple"); + EXPECT_EQ(c_api->HardwareDevice_VendorId(c_api->EpDevice_Device(ep_device)), OrtDevice::VendorIds::APPLE); + EXPECT_EQ(ep_device->ep_factory->GetVendorId(ep_device->ep_factory), OrtDevice::VendorIds::APPLE); + } + + // Both the "test.ep_to_select" auto-selection path and the AppendExecutionProvider_V2 path select CoreML as + // the only non-CPU EP. Disabling CPU fallback makes session creation fail unless CoreML takes the entire graph. + // The V2 path also checks the input assignment directly. CoreMLEPPreferNpu and CoreMLEPPreferGpu perform the + // same assignment check for policy-based selection. + RunBasicTest(kCoreMLExecutionProvider, std::nullopt, Ort::KeyValuePairs{}, /*select_devices*/ nullptr, + /*test_auto_select*/ true, AssertMul1SessionRunsOnCoreML, /*disable_cpu_ep_fallback*/ true); +} + +// Select the CoreML NPU and GPU in a single AppendExecutionProvider_V2 call without specifying MLComputeUnits. +// The factory must derive MLComputeUnits=ALL from the selected devices, and session creation must succeed. +TEST(AutoEpSelection, CoreMLEPMultipleDevices) { + const HardwareDeviceTypes hardware = GetHardwareDeviceTypes(); + if (!CoreMLCanClaimNpu(hardware) || !CoreMLCanClaimGpu(hardware)) { + GTEST_SKIP() << "Need both an NPU and a GPU that the CoreML EP can advertise on this machine."; + } + + const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); + ASSERT_NE(coreml_devices.npu, nullptr) << "CoreML EP did not claim the NPU hardware device."; + ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not claim the GPU hardware device."; + + const auto select_devices = [&](std::vector& devices) { + devices = {coreml_devices.npu, coreml_devices.gpu}; + }; + + // MLComputeUnits is intentionally omitted. If ALL were supplied explicitly, the factory would validate it instead + // of deriving it from the selected NPU and GPU. The separate CoreMLEPDefaultComputeUnits test checks this + // derivation directly. This test verifies the same behavior during session creation. + // CoreMLEPNarrowingComputeUnitsOverride covers the same multi-device V2 path with non-empty ep_options. + // select_devices applies only to the AppendExecutionProvider_V2 path. The "test.ep_to_select" path passes only the + // first CoreML device to the factory. That would test only one device, which CoreMLEP already covers, rather than + // a selection containing both the NPU and GPU. Run only the V2 path, verify its input assignment directly, and + // disable ORT CPU fallback so CoreML must execute the entire graph. + RunBasicTest(kCoreMLExecutionProvider, std::nullopt, Ort::KeyValuePairs{}, select_devices, + /*test_auto_select*/ false, AssertMul1SessionRunsOnCoreML, /*disable_cpu_ep_fallback*/ true); +} + +// Verifies that AppendExecutionProvider_V2 passes both selected devices to the factory. CPUAndGPU is accepted only +// when the GPU is present in the factory's device list, and CPUAndNeuralEngine only when the NPU is present. +// Both sessions must be created successfully, so the test fails if either device is missing. +TEST(AutoEpSelection, CoreMLEPMultipleDevicesBothReachTheFactory) { + const HardwareDeviceTypes hardware = GetHardwareDeviceTypes(); + if (!CoreMLCanClaimNpu(hardware) || !CoreMLCanClaimGpu(hardware)) { + GTEST_SKIP() << "Need both an NPU and a GPU that the CoreML EP can advertise on this machine."; + } + + const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); + ASSERT_NE(coreml_devices.npu, nullptr) << "CoreML EP did not advertise the NPU hardware device."; + ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not advertise the GPU hardware device."; + + std::vector ep_devices; + ep_devices.emplace_back(coreml_devices.npu); + ep_devices.emplace_back(coreml_devices.gpu); + + for (const char* compute_units : {kCoremlComputeUnits_CPUAndGPU, kCoremlComputeUnits_CPUAndNeuralEngine}) { + SCOPED_TRACE(compute_units); + + Ort::KeyValuePairs ep_options; + ep_options.Add(kCoremlProviderOption_MLComputeUnits, compute_units); + + Ort::SessionOptions session_options; + session_options.AppendExecutionProvider_V2(*ort_env, ep_devices, ep_options); + + // AppendExecutionProvider_V2 adds both devices and the current MLComputeUnits value to session_options. + // Session creation must succeed because MLComputeUnits enables an accelerator included in the selection. + Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); + ASSERT_NO_FATAL_FAILURE(AssertMul1SessionRunsOnCoreML(session)); + } +} + +// Select only the CoreML GPU device explicitly via AppendExecutionProvider_V2 (the CPUAndGPU default path). +TEST(AutoEpSelection, CoreMLEPExplicitGpu) { + if (!CoreMLCanClaimGpu(GetHardwareDeviceTypes())) { + GTEST_SKIP() << "No GPU hardware device that the CoreML EP factory can advertise on this machine."; + } + + const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); + ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not claim the GPU hardware device."; + + const auto select_devices = [&](std::vector& devices) { + devices = {coreml_devices.gpu}; + }; + + // select_devices affects only the AppendExecutionProvider_V2 path. The "test.ep_to_select" path selects + // only the first CoreML device, so it cannot guarantee a GPU-only selection. Verify that the session uses + // CoreML, and disable ORT CPU fallback to prevent a false pass. + RunBasicTest(kCoreMLExecutionProvider, std::nullopt, Ort::KeyValuePairs{}, select_devices, + /*test_auto_select*/ false, AssertMul1SessionRunsOnCoreML, /*disable_cpu_ep_fallback*/ true); +} + +// PREFER_NPU must select the CoreML EP for the Apple Neural Engine instead of falling back to the ORT CPU EP. +// MAX_EFFICIENCY and MIN_OVERALL_POWER use the same selector as PREFER_NPU, so the test verifies the same result +// for both aliases. +TEST(AutoEpSelection, CoreMLEPPreferNpu) { + if (!CoreMLCanClaimNpu(GetHardwareDeviceTypes())) { + GTEST_SKIP() << "No NPU hardware device, or this Core ML version cannot advertise it."; + } + ASSERT_NE(GetCoreMLEpDevices().npu, nullptr) << "CoreML EP did not claim the NPU hardware device."; + + for (const OrtExecutionProviderDevicePolicy policy : {OrtExecutionProviderDevicePolicy_PREFER_NPU, + OrtExecutionProviderDevicePolicy_MAX_EFFICIENCY, + OrtExecutionProviderDevicePolicy_MIN_OVERALL_POWER}) { + SCOPED_TRACE(testing::Message() << "policy " << policy); + Ort::SessionOptions session_options; + session_options.SetEpSelectionPolicy(policy); + Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); + + ASSERT_NO_FATAL_FAILURE(AssertMul1SessionRunsOnCoreML(session)); + } +} + +// PREFER_GPU must select the CoreML EP. When the internal WebGPU EP is also registered, both EPs advertise an +// OrtEpDevice for the same Apple GPU, and the test verifies that CoreML is selected over WebGPU. +// Without the internal WebGPU EP, the test simply verifies that PREFER_GPU selects CoreML. +// MAX_PERFORMANCE uses the same selector and is covered by the same assertions. +TEST(AutoEpSelection, CoreMLEPPreferGpu) { + if (!CoreMLCanClaimGpu(GetHardwareDeviceTypes())) { + GTEST_SKIP() << "No GPU hardware device that the CoreML EP factory can advertise on this machine."; + } + + const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); + ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not claim the GPU hardware device."; + +#if defined(USE_WEBGPU) && !defined(ORT_USE_EP_API_ADAPTERS) + // Verify that WebGPU also advertises an OrtEpDevice for the same GPU. Otherwise, selecting CoreML would not show + // that it is preferred over WebGPU for the same hardware device. + bool matching_webgpu_device_present = false; + { + const OrtApi* c_api = &Ort::GetApi(); + const OrtHardwareDevice* coreml_gpu_hw = c_api->EpDevice_Device(coreml_devices.gpu); + const OrtEpDevice* const* ep_devices = nullptr; + size_t num_devices = 0; + + ASSERT_ORTSTATUS_OK(c_api->GetEpDevices(*ort_env, &ep_devices, &num_devices)); + for (size_t i = 0; i < num_devices; ++i) { + if (strcmp(c_api->EpDevice_EpName(ep_devices[i]), kWebGpuExecutionProvider) == 0 && + c_api->EpDevice_Device(ep_devices[i]) == coreml_gpu_hw) { + matching_webgpu_device_present = true; + break; + } + } + } + + ASSERT_TRUE(matching_webgpu_device_present) + << "Expected WebGPU to also advertise an OrtEpDevice for the CoreML GPU's hardware device in this build."; +#endif + + for (const OrtExecutionProviderDevicePolicy policy : {OrtExecutionProviderDevicePolicy_PREFER_GPU, + OrtExecutionProviderDevicePolicy_MAX_PERFORMANCE}) { + SCOPED_TRACE(testing::Message() << "policy " << policy); + Ort::SessionOptions session_options; + session_options.SetEpSelectionPolicy(policy); + Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); + + ASSERT_NO_FATAL_FAILURE(AssertMul1SessionRunsOnCoreML(session)); + } +} + +// A session created through the legacy AppendExecutionProvider API has no selected OrtEpDevice. +// SessionGetEpDeviceForInputs therefore matches the assigned EP name against the OrtEpDevices returned by +// GetEpDevices and reports the first match. Before CoreML was exposed through OrtEpFactory, GetEpDevices returned +// no CoreML OrtEpDevices, so this lookup found no match and the API reported nullptr. With the factory registered, +// the lookup reports the first advertised CoreML device, which may be an NPU or a GPU. +// Because the lookup matches only the EP name, the reported device does not reflect the session's MLComputeUnits. +// A CPUOnly session therefore reports the first advertised CoreML device. With the current factory, that device is +// an NPU or a GPU. Advertising a CPU device would not resolve this mismatch because the lookup would still return +// the first CoreML device in GetEpDevices, regardless of MLComputeUnits. This test captures the current behavior, +// not a final API contract. It remains undecided which device, if any, a legacy CoreML session should report. +TEST(AutoEpSelection, CoreMLLegacySessionReportsFirstAdvertisedEpDevice) { + const OrtEpDevice* first_coreml_device = GetFirstCoreMLEpDevice(); + if (first_coreml_device == nullptr) { + GTEST_SKIP() << "CoreML EP advertised no OrtEpDevice on this machine, so the lookup has nothing to report."; + } + + Ort::SessionOptions session_options; + session_options.AppendExecutionProvider( + kCoreMLExecutionProvider, {{kCoremlProviderOption_MLComputeUnits, kCoremlComputeUnits_CPUOnly}}); + Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); + + const OrtApi* c_api = &Ort::GetApi(); + const OrtEpDevice* input_ep_device = nullptr; + ASSERT_ORTSTATUS_OK(c_api->SessionGetEpDeviceForInputs(session, &input_ep_device, 1)); + + ASSERT_NE(input_ep_device, nullptr) << "Expected the legacy CoreML session to report an OrtEpDevice."; + EXPECT_STREQ(c_api->EpDevice_EpName(input_ep_device), kCoreMLExecutionProvider); + EXPECT_EQ(input_ep_device, first_coreml_device) + << "The legacy session has no selected OrtEpDevice, so the current name-based lookup should report the first " + "advertised CoreML device."; + + // SessionGetEpDeviceForOutputs performs its own device lookup, so check it separately from + // SessionGetEpDeviceForInputs. + const OrtEpDevice* output_ep_device = nullptr; + ASSERT_ORTSTATUS_OK(c_api->SessionGetEpDeviceForOutputs(session, &output_ep_device, 1)); + + ASSERT_NE(output_ep_device, nullptr) + << "Expected SessionGetEpDeviceForOutputs to report an OrtEpDevice for the legacy CoreML session."; + EXPECT_STREQ(c_api->EpDevice_EpName(output_ep_device), kCoreMLExecutionProvider); + EXPECT_EQ(output_ep_device, first_coreml_device) + << "SessionGetEpDeviceForOutputs must report the same first advertised CoreML device as " + "SessionGetEpDeviceForInputs."; +} + +// Verifies how the factory sets MLComputeUnits when the caller does not specify it: CPUAndNeuralEngine for NPU, +// CPUAndGPU for GPU, and ALL for NPU plus GPU. Also verifies that a compatible user-provided value is preserved. +// The test uses synthetic hardware devices, so it also runs on Apple systems where discovery reports no NPU or +// GPU, such as Intel Macs. +TEST(AutoEpSelection, CoreMLEPDefaultComputeUnits) { + OrtHardwareDevice npu{}; + npu.type = OrtHardwareDeviceType_NPU; + OrtHardwareDevice gpu{}; + gpu.type = OrtHardwareDeviceType_GPU; + + const auto default_compute_units = [](std::initializer_list devices) -> std::string { + ProviderOptions options; + Ort::Status status{CoreMLEpFactory::ValidateDeviceSelectionAndResolveMLComputeUnits( + devices.begin(), devices.size(), options)}; + if (!status.IsOK()) { + ADD_FAILURE() << status.GetErrorMessage(); + return {}; + } + + const auto it = options.find(kCoremlProviderOption_MLComputeUnits); + if (it == options.end()) { + ADD_FAILURE() << "MLComputeUnits was not set."; + return {}; + } + + return it->second; + }; + + EXPECT_EQ(default_compute_units({&npu}), "CPUAndNeuralEngine"); + EXPECT_EQ(default_compute_units({&gpu}), "CPUAndGPU"); + EXPECT_EQ(default_compute_units({&npu, &gpu}), "ALL"); + + // A compatible user-provided MLComputeUnits value is preserved. + ProviderOptions user_options{{kCoremlProviderOption_MLComputeUnits, "CPUOnly"}}; + const OrtHardwareDevice* npu_only[] = {&npu}; + Ort::Status status{ + CoreMLEpFactory::ValidateDeviceSelectionAndResolveMLComputeUnits(npu_only, 1, user_options)}; + ASSERT_TRUE(status.IsOK()) << status.GetErrorMessage(); + EXPECT_EQ(user_options[kCoremlProviderOption_MLComputeUnits], "CPUOnly"); +} + +// Verifies that MLComputeUnits reaches the factory through SessionOptions. With PREFER_NPU, +// MLComputeUnits=CPUAndGPU must be rejected because the policy did not select the GPU. +TEST(AutoEpSelection, CoreMLEPPreferNpuConflictingComputeUnits) { + if (!CoreMLCanClaimNpu(GetHardwareDeviceTypes())) { + GTEST_SKIP() << "No NPU hardware device, or this Core ML version cannot advertise it."; + } + ASSERT_NE(GetCoreMLEpDevices().npu, nullptr) << "CoreML EP did not claim the NPU hardware device."; + + Ort::SessionOptions session_options; + session_options.SetEpSelectionPolicy(OrtExecutionProviderDevicePolicy_PREFER_NPU); + + const std::string option_key = + OrtSessionOptions::GetProviderOptionPrefix(kCoreMLExecutionProvider) + kCoremlProviderOption_MLComputeUnits; + session_options.AddConfigEntry(option_key.c_str(), "CPUAndGPU"); + + try { + Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); + FAIL() << "Session creation should have failed: MLComputeUnits=CPUAndGPU conflicts with the NPU selection."; + } catch (const Ort::Exception& ex) { + EXPECT_EQ(ex.GetOrtErrorCode(), ORT_INVALID_ARGUMENT); + EXPECT_THAT(ex.what(), ::testing::HasSubstr("was not selected")); + } +} + +// Verifies that AppendExecutionProvider_V2 passes ep_options to factory validation. With an explicit GPU-only +// selection, MLComputeUnits=CPUAndNeuralEngine must be rejected because the NPU was not selected. +TEST(AutoEpSelection, CoreMLEPExplicitGpuConflictingComputeUnits) { + if (!CoreMLCanClaimGpu(GetHardwareDeviceTypes())) { + GTEST_SKIP() << "No GPU hardware device that the CoreML EP factory can advertise on this machine."; + } + + const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); + ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not claim the GPU hardware device."; + + Ort::SessionOptions session_options; + Ort::KeyValuePairs ep_options; + ep_options.Add(kCoremlProviderOption_MLComputeUnits, "CPUAndNeuralEngine"); + std::vector ep_devices{Ort::ConstEpDevice{coreml_devices.gpu}}; + session_options.AppendExecutionProvider_V2(*ort_env, ep_devices, ep_options); + + try { + Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); + FAIL() << "Session creation should have failed: MLComputeUnits=CPUAndNeuralEngine conflicts with the GPU " + "selection."; + } catch (const Ort::Exception& ex) { + // Verify the diagnostic message independently of the V2 error-code handling. + // CoreMLEPPreferNpuConflictingComputeUnits verifies ORT_INVALID_ARGUMENT through the policy path. + EXPECT_THAT(ex.what(), ::testing::HasSubstr("was not selected")); + } +} + +// Verifies that an unknown MLComputeUnits value reaches CoreMLOptions and is rejected during provider creation. +// CoreMLEPComputeUnitsOverrideValidation separately verifies that the factory passes the value through. +TEST(AutoEpSelection, CoreMLEPUnknownComputeUnitsRejectedByProvider) { + const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); + const OrtEpDevice* device = coreml_devices.npu != nullptr ? coreml_devices.npu : coreml_devices.gpu; + if (device == nullptr) { + GTEST_SKIP() << "No CoreML OrtEpDevice on this machine."; + } + + Ort::KeyValuePairs ep_options; + ep_options.Add(kCoremlProviderOption_MLComputeUnits, "NotAComputeUnitsValue"); + std::vector ep_devices{Ort::ConstEpDevice{device}}; + + Ort::SessionOptions session_options; + session_options.AppendExecutionProvider_V2(*ort_env, ep_devices, ep_options); + + try { + Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); + FAIL() << "Session creation should have failed: MLComputeUnits=NotAComputeUnitsValue is not a valid value."; + } catch (const Ort::Exception& ex) { + EXPECT_THAT(ex.what(), ::testing::HasSubstr("Invalid value for option")); + EXPECT_THAT(ex.what(), ::testing::HasSubstr("NotAComputeUnitsValue")); + } +} + +// Verifies that MLComputeUnits=CPUOnly can narrow an accelerator selection. The session must be created and run +// successfully, and the graph must remain assigned to the CoreML EP. CoreML may enable only accelerators represented +// by the selected OrtEpDevices. This does not prevent Core ML from using its internal CPU path. +// MLComputeUnits=CPUOnly also does not reassign the graph to the ORT CPU EP. +TEST(AutoEpSelection, CoreMLEPNarrowingComputeUnitsOverride) { + const HardwareDeviceTypes hardware = GetHardwareDeviceTypes(); + const bool can_claim_npu = CoreMLCanClaimNpu(hardware); + const bool can_claim_gpu = CoreMLCanClaimGpu(hardware); + if (!can_claim_npu && !can_claim_gpu) { + GTEST_SKIP() << "No hardware device that the CoreML EP factory can advertise on this machine."; + } + + const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); + std::vector ep_devices; + if (can_claim_npu) { + ASSERT_NE(coreml_devices.npu, nullptr) << "CoreML EP did not claim the NPU hardware device."; + ep_devices.emplace_back(coreml_devices.npu); + } + if (can_claim_gpu) { + ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not claim the GPU hardware device."; + ep_devices.emplace_back(coreml_devices.gpu); + } + + Ort::KeyValuePairs ep_options; + ep_options.Add(kCoremlProviderOption_MLComputeUnits, "CPUOnly"); + + Ort::SessionOptions session_options; + session_options.AppendExecutionProvider_V2(*ort_env, ep_devices, ep_options); + Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); + + ASSERT_NO_FATAL_FAILURE(AssertMul1SessionRunsOnCoreML(session)); +} + +// Verifies that a user-provided MLComputeUnits value is compatible with the selected accelerators. The value +// may disable selected accelerators, as CPUOnly does, but it must not enable an accelerator that was not selected. +TEST(AutoEpSelection, CoreMLEPComputeUnitsOverrideValidation) { + OrtHardwareDevice npu{}; + npu.type = OrtHardwareDeviceType_NPU; + OrtHardwareDevice gpu{}; + gpu.type = OrtHardwareDeviceType_GPU; + + const auto validate = [](std::initializer_list devices, + std::string_view compute_units) { + ProviderOptions options{{kCoremlProviderOption_MLComputeUnits, std::string{compute_units}}}; + return Ort::Status{CoreMLEpFactory::ValidateDeviceSelectionAndResolveMLComputeUnits( + devices.begin(), devices.size(), options)}; + }; + + // Values that enable only selected accelerators, or disable them with CPUOnly, are allowed. + EXPECT_TRUE(validate({&npu}, "CPUAndNeuralEngine").IsOK()); + EXPECT_TRUE(validate({&npu}, "CPUOnly").IsOK()); + EXPECT_TRUE(validate({&gpu}, "CPUAndGPU").IsOK()); + EXPECT_TRUE(validate({&gpu}, "CPUOnly").IsOK()); + EXPECT_TRUE(validate({&npu, &gpu}, "ALL").IsOK()); + EXPECT_TRUE(validate({&npu, &gpu}, "CPUAndNeuralEngine").IsOK()); + EXPECT_TRUE(validate({&npu, &gpu}, "CPUAndGPU").IsOK()); + EXPECT_TRUE(validate({&npu, &gpu}, "CPUOnly").IsOK()); + + // Values that enable an unselected accelerator are rejected. + ExpectInvalidArgument(validate({&npu}, "CPUAndGPU"), "NPU selected, CPUAndGPU requested"); + ExpectInvalidArgument(validate({&npu}, "ALL"), "NPU selected, ALL requested"); + ExpectInvalidArgument(validate({&gpu}, "CPUAndNeuralEngine"), "GPU selected, CPUAndNeuralEngine requested"); + ExpectInvalidArgument(validate({&gpu}, "ALL"), "GPU selected, ALL requested"); + + // This factory helper checks only whether MLComputeUnits is compatible with the selected devices. + // Unknown values are left for CoreMLOptions to reject during provider creation. + EXPECT_TRUE(validate({&npu}, "NotAComputeUnitsValue").IsOK()); +} + +// Tests which device selections the factory accepts or rejects. +// Valid selections contain one NPU, one GPU, or one NPU plus one GPU. +// Empty lists, CPU devices, duplicate device types, a null device entry, and a null device-list pointer with a +// nonzero device count must return ORT_INVALID_ARGUMENT. +TEST(AutoEpSelection, CoreMLEPDeviceValidation) { + OrtHardwareDevice npu{}; + npu.type = OrtHardwareDeviceType_NPU; + OrtHardwareDevice gpu{}; + gpu.type = OrtHardwareDeviceType_GPU; + OrtHardwareDevice cpu{}; + cpu.type = OrtHardwareDeviceType_CPU; + + const auto validate = [](std::initializer_list devices) { + ProviderOptions options; + return Ort::Status{CoreMLEpFactory::ValidateDeviceSelectionAndResolveMLComputeUnits( + devices.begin(), devices.size(), options)}; + }; + + // The valid selections, for contrast. + EXPECT_TRUE(validate({&npu}).IsOK()); + EXPECT_TRUE(validate({&gpu}).IsOK()); + EXPECT_TRUE(validate({&npu, &gpu}).IsOK()); + + ExpectInvalidArgument(validate({}), "no devices"); + ExpectInvalidArgument(validate({&cpu}), "CPU only: the factory never claims the CPU device"); + ExpectInvalidArgument(validate({&cpu, &gpu}), "GPU mixed with an unsupported device"); + ExpectInvalidArgument(validate({&npu, &npu}), "duplicate NPU"); + ExpectInvalidArgument(validate({&gpu, &gpu}), "duplicate GPU"); + + ExpectInvalidArgument(validate({nullptr}), "null device"); + ProviderOptions options; + const Ort::Status null_device_list_status{ + CoreMLEpFactory::ValidateDeviceSelectionAndResolveMLComputeUnits(nullptr, 1, options)}; + ExpectInvalidArgument(null_device_list_status, "null device list"); +} + +// Verifies CanClaimDeviceType independently of the host hardware and runtime Core ML version. +// This test covers the Core ML 5 minimum, the Core ML 6 NPU requirement, and the version-independent CPU exclusion. +TEST(AutoEpSelection, CoreMLEPClaimabilityByVersion) { + const auto can_claim = [](OrtHardwareDeviceType type, int32_t coreml_version) { + return CoreMLEpFactory::CanClaimDeviceType(type, coreml_version); + }; + + // Below the EP's Core ML 5 minimum (provider creation would fail) nothing is advertised. + EXPECT_FALSE(can_claim(OrtHardwareDeviceType_NPU, 4)); + EXPECT_FALSE(can_claim(OrtHardwareDeviceType_GPU, 4)); + + // Core ML 5: the GPU is advertised, the NPU is not (CPUAndNeuralEngine requires Core ML 6). + EXPECT_FALSE(can_claim(OrtHardwareDeviceType_NPU, 5)); + EXPECT_TRUE(can_claim(OrtHardwareDeviceType_GPU, 5)); + + // Core ML 6+: both. + EXPECT_TRUE(can_claim(OrtHardwareDeviceType_NPU, 6)); + EXPECT_TRUE(can_claim(OrtHardwareDeviceType_GPU, 6)); + EXPECT_TRUE(can_claim(OrtHardwareDeviceType_NPU, 7)); + EXPECT_TRUE(can_claim(OrtHardwareDeviceType_GPU, 7)); + + // The CPU device is never claimed regardless of version (left to the CPU EP). + EXPECT_FALSE(can_claim(OrtHardwareDeviceType_CPU, 4)); + EXPECT_FALSE(can_claim(OrtHardwareDeviceType_CPU, 8)); +} + +// Tests SelectDevicesToClaim with synthetic hardware devices instead of relying on host discovery. Covers CPU +// filtering, input order, duplicate devices of the same type, the max_ep_devices limit, and version checks +// independently of the Core ML version installed on the host. +TEST(AutoEpSelection, CoreMLEPSelectDevicesToClaim) { + OrtHardwareDevice npu0{}; + npu0.type = OrtHardwareDeviceType_NPU; + OrtHardwareDevice npu1{}; + npu1.type = OrtHardwareDeviceType_NPU; + OrtHardwareDevice gpu0{}; + gpu0.type = OrtHardwareDeviceType_GPU; + OrtHardwareDevice gpu1{}; + gpu1.type = OrtHardwareDeviceType_GPU; + OrtHardwareDevice cpu{}; + cpu.type = OrtHardwareDeviceType_CPU; + + constexpr int32_t coreml6 = 6; + const auto select = [](std::initializer_list devices, int32_t coreml_version, + size_t max_ep_devices = 8) { + return CoreMLEpFactory::SelectDevicesToClaim(gsl::make_span(devices.begin(), devices.size()), coreml_version, + max_ep_devices); + }; + + // CPU + NPU + GPU -> the CPU is filtered out. The NPU and GPU remain in input order. + { + const auto selected = select({&cpu, &npu0, &gpu0}, coreml6); + ASSERT_EQ(selected.size(), 2u); + EXPECT_EQ(selected[0], &npu0); + EXPECT_EQ(selected[1], &gpu0); + } + + // Repeat the NPU+GPU selection with the GPU first. Together with the NPU-first case above, this verifies that + // selection preserves input order instead of sorting by device type. + { + const auto selected = select({&cpu, &gpu0, &npu0}, coreml6); + ASSERT_EQ(selected.size(), 2u); + EXPECT_EQ(selected[0], &gpu0); + EXPECT_EQ(selected[1], &npu0); + } + + // Duplicate devices of a type: only the first of each type is advertised. + { + const auto selected = select({&gpu0, &gpu1}, coreml6); + ASSERT_EQ(selected.size(), 1u); + EXPECT_EQ(selected[0], &gpu0); + } + { + const auto selected = select({&npu0, &npu1, &gpu0, &gpu1}, coreml6); + ASSERT_EQ(selected.size(), 2u); + EXPECT_EQ(selected[0], &npu0); + EXPECT_EQ(selected[1], &gpu0); + } + + // max_ep_devices limits the number of selected devices. + { + const auto selected = select({&npu0, &gpu0}, coreml6, /*max_ep_devices*/ 1); + ASSERT_EQ(selected.size(), 1u); + EXPECT_EQ(selected[0], &npu0); + } + + // Verify that selection applies the CanClaimDeviceType version checks. + { + const auto selected = select({&npu0, &gpu0}, /*coreml_version*/ 5); + ASSERT_EQ(selected.size(), 1u); + EXPECT_EQ(selected[0], &gpu0); + } + EXPECT_TRUE(select({&npu0, &gpu0}, /*coreml_version*/ 4).empty()); +} + +// Tests CreateAndPublishEpDevices with injected create and release callbacks. On failure, all previously +// created devices must be released and the output parameters must remain unchanged. On success, all device +// pointers must be published without releasing any devices. The dummy OrtEpDevice objects are never dereferenced. +// Their addresses identify which devices are published or released. +TEST(AutoEpSelection, CoreMLEPCreateAndPublishEpDevices) { + OrtHardwareDevice npu{}; + npu.type = OrtHardwareDeviceType_NPU; + OrtHardwareDevice gpu{}; + gpu.type = OrtHardwareDeviceType_GPU; + const std::array selected{&npu, &gpu}; + + OrtEpDevice dummy_device0{}; + OrtEpDevice dummy_device1{}; + + InlinedVector released; + const auto release = [&released](OrtEpDevice* ep_device) { released.push_back(ep_device); }; + + // Initialize the caller-owned output array with distinct non-null pointers. Failure cases verify that these values + // remain unchanged. Initializing the array with nullptr would not distinguish "left untouched" from + // "cleared to nullptr". + OrtEpDevice sentinel0{}; + OrtEpDevice sentinel1{}; + const std::array untouched{&sentinel0, &sentinel1}; + + // The second device creation fails. The first device must be released, and the output parameters must remain + // unchanged. + { + size_t create_calls = 0; + const auto create_second_fails = [&](const OrtHardwareDevice&, OrtEpDevice** ep_device) -> OrtStatus* { + if (create_calls++ == 0) { + *ep_device = &dummy_device0; + return nullptr; + } + + return Ort::GetApi().CreateStatus(ORT_FAIL, "injected creation failure"); + }; + + std::array ep_devices = untouched; + size_t num_ep_devices = 42; // Initial value that must remain unchanged on failure. + + Ort::Status status{CoreMLEpFactory::CreateAndPublishEpDevices(gsl::make_span(selected), create_second_fails, + release, ep_devices.data(), &num_ep_devices)}; + ASSERT_FALSE(status.IsOK()); + EXPECT_THAT(status.GetErrorMessage(), ::testing::HasSubstr("injected creation failure")); + EXPECT_THAT(released, ::testing::ElementsAre(&dummy_device0)); + EXPECT_THAT(ep_devices, ::testing::ElementsAreArray(untouched)); + EXPECT_EQ(num_ep_devices, size_t{42}); + } + + // The first device creation fails. No devices need to be released, and the output parameters must remain unchanged. + { + released.clear(); + const auto create_first_fails = [](const OrtHardwareDevice&, OrtEpDevice**) -> OrtStatus* { + return Ort::GetApi().CreateStatus(ORT_FAIL, "injected creation failure"); + }; + + std::array ep_devices = untouched; + size_t num_ep_devices = 42; // Initial value that must remain unchanged on failure. + + Ort::Status status{CoreMLEpFactory::CreateAndPublishEpDevices(gsl::make_span(selected), create_first_fails, + release, ep_devices.data(), &num_ep_devices)}; + ASSERT_FALSE(status.IsOK()); + EXPECT_TRUE(released.empty()); + EXPECT_THAT(ep_devices, ::testing::ElementsAreArray(untouched)); + EXPECT_EQ(num_ep_devices, size_t{42}); + } + + // Success: both devices are published, nothing is released. + { + released.clear(); + size_t create_calls = 0; + const auto create_ok = [&](const OrtHardwareDevice&, OrtEpDevice** ep_device) -> OrtStatus* { + *ep_device = (create_calls++ == 0) ? &dummy_device0 : &dummy_device1; + return nullptr; + }; + + std::array ep_devices{nullptr, nullptr}; + size_t num_ep_devices = 0; + + Ort::Status status{CoreMLEpFactory::CreateAndPublishEpDevices(gsl::make_span(selected), create_ok, release, + ep_devices.data(), &num_ep_devices)}; + ASSERT_TRUE(status.IsOK()) << status.GetErrorMessage(); + EXPECT_EQ(num_ep_devices, size_t{2}); + EXPECT_EQ(ep_devices[0], &dummy_device0); + EXPECT_EQ(ep_devices[1], &dummy_device1); + EXPECT_TRUE(released.empty()); + } + + // No devices selected: a successful call still sets the output count to zero, as required by GetSupportedDevices. + { + released.clear(); + const auto create_never_called = [](const OrtHardwareDevice&, OrtEpDevice**) -> OrtStatus* { + ADD_FAILURE() << "create_ep_device must not be called when nothing was selected."; + return Ort::GetApi().CreateStatus(ORT_FAIL, "unexpected call"); + }; + + std::array ep_devices = untouched; + size_t num_ep_devices = 42; + + Ort::Status status{CoreMLEpFactory::CreateAndPublishEpDevices(gsl::span{}, + create_never_called, release, ep_devices.data(), + &num_ep_devices)}; + ASSERT_TRUE(status.IsOK()) << status.GetErrorMessage(); + EXPECT_EQ(num_ep_devices, size_t{0}); + EXPECT_THAT(ep_devices, ::testing::ElementsAreArray(untouched)); + EXPECT_TRUE(released.empty()); + } +} + +// Verifies that OrderDevices ranks CoreML ahead of another EP for the same Apple GPU because of vendor affinity. +// CoreMLEPPreferGpu does not test this rule independently because both vendor affinity and EP-name ordering rank +// CoreML ahead of WebGPU. This test uses a competing EP whose name sorts before CoreML, so CoreML can rank first +// only because its Apple vendor identity matches the GPU. +TEST(AutoEpSelection, CoreMLVendorAffinityPrecedesEpNameOrdering) { + // The competitor's name must sort before CoreML's. Otherwise, EP-name ordering could also put CoreML first, and + // the test would not verify vendor affinity independently. + constexpr const char* kCompetitorEpName = "AaaExecutionProvider"; + ASSERT_LT(std::string_view{kCompetitorEpName}, std::string_view{kCoreMLExecutionProvider}) + << "The competitor EP name must sort before the CoreML EP name for this test to verify vendor affinity " + "independently."; + + // Both OrtEpDevices reference the same Apple GPU, so their hardware properties are identical. Only the CoreML + // factory's vendor matches the GPU, so vendor affinity determines their order. + OrtHardwareDevice apple_gpu{}; + apple_gpu.type = OrtHardwareDeviceType_GPU; + apple_gpu.vendor_id = OrtDevice::VendorIds::APPLE; + apple_gpu.vendor = "Apple"; + + OrtEpFactory apple_factory{}; + apple_factory.GetVendorId = [](const OrtEpFactory*) noexcept -> uint32_t { + return OrtDevice::VendorIds::APPLE; + }; + + OrtEpFactory competitor_factory{}; + competitor_factory.GetVendorId = [](const OrtEpFactory*) noexcept -> uint32_t { + return OrtDevice::VendorIds::MICROSOFT; + }; + + OrtEpDevice coreml_device{}; + coreml_device.ep_name = kCoreMLExecutionProvider; + coreml_device.ep_vendor = "Apple"; + coreml_device.device = &apple_gpu; + coreml_device.ep_factory = &apple_factory; + + OrtEpDevice competitor_device{}; + competitor_device.ep_name = kCompetitorEpName; + competitor_device.ep_vendor = "Microsoft"; + competitor_device.device = &apple_gpu; + competitor_device.ep_factory = &competitor_factory; + + // Test both input orders. CoreML must be placed first in both cases, confirming that vendor affinity places it + // ahead regardless of input order. + ProviderPolicyContext context; + for (const bool coreml_first : {false, true}) { + SCOPED_TRACE(testing::Message() << "coreml_first " << coreml_first); + const std::vector devices = + coreml_first ? std::vector{&coreml_device, &competitor_device} + : std::vector{&competitor_device, &coreml_device}; + + const std::vector ordered = context.OrderDevices(devices); + + ASSERT_EQ(ordered.size(), size_t{2}); + EXPECT_EQ(ordered[0], &coreml_device) + << "Vendor affinity must place CoreML ahead of a competing EP whose name sorts before CoreML's."; + EXPECT_EQ(ordered[1], &competitor_device); + } +} +#endif // defined(USE_COREML) && defined(__APPLE__) + +#if defined(USE_COREML) && !defined(__APPLE__) +// CoreML can be built on non-Apple platforms to test model conversion, but it cannot execute models there. +// The factory checks only the device type and Core ML version. Because the non-Apple CoreMLVersion stub reports +// Core ML 7, the factory would accept accelerators discovered on a non-Apple system. Verify that the platform guards +// prevent the CoreML factory from being registered on non-Apple builds. +// Registration is checked with GetHardwareDeviceEpIncompatibilityDetails rather than through the published device +// list. The function looks up the factory by EP name before checking hardware compatibility, so any discovered +// device, including the CPU, is sufficient and the test does not depend on accelerator discovery. +TEST(AutoEpSelection, CoreMLEPIsNotRegisteredOnNonApplePlatforms) { + const OrtApi* c_api = &Ort::GetApi(); + + size_t num_devices = 0; + ASSERT_ORTSTATUS_OK(c_api->GetNumHardwareDevices(*ort_env, &num_devices)); + ASSERT_GT(num_devices, 0u) << "Expected device discovery to report at least the CPU device."; + + std::vector devices(num_devices); + ASSERT_ORTSTATUS_OK(c_api->GetHardwareDevices(*ort_env, devices.data(), num_devices)); + + OrtDeviceEpIncompatibilityDetails* details = nullptr; + Ort::Status status{c_api->GetHardwareDeviceEpIncompatibilityDetails(*ort_env, kCoreMLExecutionProvider, + devices.front(), &details)}; + // If the CoreML factory is unexpectedly registered, the call succeeds and returns an object that must be released + // before the test reports the failure. + if (details != nullptr) { + c_api->ReleaseDeviceEpIncompatibilityDetails(details); + } + + ASSERT_FALSE(status.IsOK()) << "A factory named " << kCoreMLExecutionProvider << " is registered on a non-Apple " + << "platform, where CoreML cannot execute models."; + EXPECT_EQ(status.GetErrorCode(), ORT_INVALID_ARGUMENT); + // ORT_INVALID_ARGUMENT alone does not prove that the factory is absent. If the EP name matches a registered + // factory, GetHardwareDeviceEpIncompatibilityDetails invokes the factory's compatibility callback, + // which may return the same error code. Verify the diagnostic to confirm that no factory matched the CoreML EP name. + EXPECT_THAT(status.GetErrorMessage(), ::testing::HasSubstr("No valid factory found for execution provider")); +} +#endif // defined(USE_COREML) && !defined(__APPLE__) + // tests for AutoEP selection related things in the API that aren't covered by the other tests. TEST(AutoEpSelection, MiscApiTests) { const OrtApi* c_api = &Ort::GetApi(); diff --git a/onnxruntime/test/python/onnxruntime_test_python.py b/onnxruntime/test/python/onnxruntime_test_python.py index d60acfc8bc34f..fd39e1b994dc8 100644 --- a/onnxruntime/test/python/onnxruntime_test_python.py +++ b/onnxruntime/test/python/onnxruntime_test_python.py @@ -1798,6 +1798,23 @@ def test_ort_device(self): self.assertEqual(cuda_device.device_vendor_id(), onnxrt.OrtDeviceVendorId.NVIDIA) self.assertEqual(onnxrt.OrtDeviceVendorId.NVIDIA, 0x10DE) + # OrtDeviceVendorId mirrors OrtDevice::VendorIds in ortdevice.h. The CoreML factory and Apple + # hardware discovery both use VendorIds::APPLE, so the Python value must match the C++ constant. + self.assertEqual(onnxrt.OrtDeviceVendorId.APPLE, 0x106B) + + # When Apple accelerator hardware is available, also verify the vendor ID returned by get_hardware_devices(). + apple_accelerator = next( + ( + device + for device in onnxrt.get_hardware_devices() + if device.type in (onnxrt.OrtHardwareDeviceType.GPU, onnxrt.OrtHardwareDeviceType.NPU) + and device.vendor == "Apple" + ), + None, + ) + if apple_accelerator is not None: + self.assertEqual(apple_accelerator.vendor_id, onnxrt.OrtDeviceVendorId.APPLE) + def test_ort_memory_info(self): cpu_memory_info = onnxrt.OrtMemoryInfo( "Cpu", From 2f2aafd2343eff3eefcedb1c655fece2c9e804f8 Mon Sep 17 00:00:00 2001 From: Maxim Davgalev Date: Tue, 25 Aug 2026 17:20:21 -0700 Subject: [PATCH 02/12] Report Microsoft as the CoreML EP factory vendor The CoreML EP is implemented as part of ONNX Runtime, not provided by Apple, so its factory now reports "Microsoft" and VendorIds::MICROSOFT, like the other internal EP factories (CPU, DML, and WebGPU). Discovered Apple hardware devices keep their "Apple" / VendorIds::APPLE identifiers. VendorIds::APPLE remains the named constant for the vendor ID reported by Apple platform device discovery. Because the factory vendor no longer matches the Apple hardware vendor, CoreML is no longer preferred based on vendor matching during device ordering. When another EP claims the same hardware device, ordering falls back to the EP name tie-breaker. The CoreMLVendorAffinityPrecedesEpNameOrdering test was deleted because it tested the vendor affinity behavior removed by this change. The remaining vendor assertions now expect Microsoft identifiers for the factory and Apple identifiers for the hardware device. --- .../onnxruntime/core/framework/ortdevice.h | 2 +- .../session/plugin_ep/ep_factory_coreml.cc | 4 +- .../session/plugin_ep/ep_factory_coreml.h | 15 ++-- onnxruntime/test/autoep/test_selection.cc | 69 ++----------------- .../test/python/onnxruntime_test_python.py | 4 +- 5 files changed, 18 insertions(+), 76 deletions(-) diff --git a/include/onnxruntime/core/framework/ortdevice.h b/include/onnxruntime/core/framework/ortdevice.h index 11e06ff1bc8a9..afba978ee022f 100644 --- a/include/onnxruntime/core/framework/ortdevice.h +++ b/include/onnxruntime/core/framework/ortdevice.h @@ -57,7 +57,7 @@ struct OrtDevice { // No vendor ID. Valid for DeviceType::CPU + MemType::DEFAULT or for generic allocators like WebGPU. NONE = 0x0000, AMD = 0x1002, // MIGraphX EP - APPLE = 0x106B, // CoreML EP + APPLE = 0x106B, // Apple hardware via device discovery NVIDIA = 0x10DE, // CUDA/TensorRT ARM = 0x13B5, // ARM GPU EP MICROSOFT = 0x1414, // DML EP diff --git a/onnxruntime/core/session/plugin_ep/ep_factory_coreml.cc b/onnxruntime/core/session/plugin_ep/ep_factory_coreml.cc index 4291c876eec63..6fdbddf26b9f9 100644 --- a/onnxruntime/core/session/plugin_ep/ep_factory_coreml.cc +++ b/onnxruntime/core/session/plugin_ep/ep_factory_coreml.cc @@ -49,8 +49,8 @@ bool CoreMLEpFactory::CanClaimDeviceType(OrtHardwareDeviceType device_type, int3 // The CPU device is deliberately not claimed. DEFAULT and PREFER_CPU select a single preferred EP for the CPU // device. The NPU- and GPU-preferring policies first select an EP for the accelerator and then use the same CPU // selection as a fallback. - // If CoreML claimed the CPU, vendor affinity on Apple hardware would make it the preferred EP for that device, - // ahead of the ORT CPU EP and other CPU-based EPs. + // If CoreML claimed the CPU, it would compete with the ORT CPU EP and other CPU-based EPs to be selected as the + // primary CPU EP. // CoreML-on-CPU remains available through an explicit MLComputeUnits=CPUOnly option. Core ML may also use its // internal CPU path for operations unsupported by the selected compute units. return false; diff --git a/onnxruntime/core/session/plugin_ep/ep_factory_coreml.h b/onnxruntime/core/session/plugin_ep/ep_factory_coreml.h index a743a3a8ffdb4..15daddffc7abd 100644 --- a/onnxruntime/core/session/plugin_ep/ep_factory_coreml.h +++ b/onnxruntime/core/session/plugin_ep/ep_factory_coreml.h @@ -26,13 +26,14 @@ namespace onnxruntime { class CoreMLEpFactory : public EpFactoryInternalImpl { public: - // Report "Apple" and VendorIds::APPLE as the factory vendor identifiers so device ordering gives CoreML - // affinity for Apple hardware. The vendor ID is checked first, with the vendor name used as a fallback. - // Provider-bridge factories copy vendor identifiers from the OrtEpFactory they wrap. - // The CPU, DML, and WebGPU internal factories report "Microsoft". CoreML reports "Apple" through - // EpDevice_EpVendor. - // When multiple EPs target the same NPU or GPU and have equal vendor affinity, they are ordered by EP name. - CoreMLEpFactory() : EpFactoryInternalImpl(kCoreMLExecutionProvider, "Apple", OrtDevice::VendorIds::APPLE) {} + // Report "Microsoft" and VendorIds::MICROSOFT as the factory vendor identifiers, like the other internal + // factories (CPU, DML, and WebGPU): the factory vendor identifies who provides the EP implementation, not the + // hardware vendor. Discovered Apple hardware devices keep their own "Apple" / VendorIds::APPLE identifiers. + // Because the factory vendor does not match the Apple hardware vendor, device ordering gives CoreML no vendor + // affinity. When multiple EPs target the same NPU or GPU with equal vendor affinity, they are ordered by + // EP name. + CoreMLEpFactory() + : EpFactoryInternalImpl(kCoreMLExecutionProvider, "Microsoft", OrtDevice::VendorIds::MICROSOFT) {} ORT_DISALLOW_COPY_ASSIGNMENT_AND_MOVE(CoreMLEpFactory); diff --git a/onnxruntime/test/autoep/test_selection.cc b/onnxruntime/test/autoep/test_selection.cc index 106753c3f744d..4f27676393851 100644 --- a/onnxruntime/test/autoep/test_selection.cc +++ b/onnxruntime/test/autoep/test_selection.cc @@ -25,7 +25,6 @@ #include "core/providers/coreml/coreml_provider_factory.h" #include "core/providers/coreml/model/host_utils.h" #include "core/session/plugin_ep/ep_factory_coreml.h" -#include "core/session/provider_policy_context.h" #endif #include "test_allocator.h" @@ -446,8 +445,9 @@ TEST(AutoEpSelection, CoreMLEP) { ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not claim the GPU hardware device."; } - // Verify the CoreML factory's vendor name and vendor ID, along with the hardware vendor ID. Device ordering - // checks vendor IDs first and uses vendor names as a fallback. + // Verify the CoreML factory's vendor name and vendor ID, along with the hardware vendor ID. The factory reports + // "Microsoft", like the other internal factories: the factory vendor identifies who provides the EP + // implementation, not the hardware vendor. // Apple device discovery currently assigns VendorIds::APPLE to every NPU and GPU it reports. The CoreML factory // intentionally accepts any discovered GPU regardless of vendor, so this hardware vendor ID assertion must be // updated if discovery later reports a non-Apple GPU (see the multi-GPU TODO in @@ -463,9 +463,9 @@ TEST(AutoEpSelection, CoreMLEP) { continue; } - EXPECT_STREQ(c_api->EpDevice_EpVendor(ep_device), "Apple"); + EXPECT_STREQ(c_api->EpDevice_EpVendor(ep_device), "Microsoft"); EXPECT_EQ(c_api->HardwareDevice_VendorId(c_api->EpDevice_Device(ep_device)), OrtDevice::VendorIds::APPLE); - EXPECT_EQ(ep_device->ep_factory->GetVendorId(ep_device->ep_factory), OrtDevice::VendorIds::APPLE); + EXPECT_EQ(ep_device->ep_factory->GetVendorId(ep_device->ep_factory), OrtDevice::VendorIds::MICROSOFT); } // Both the "test.ep_to_select" auto-selection path and the AppendExecutionProvider_V2 path select CoreML as @@ -1093,65 +1093,6 @@ TEST(AutoEpSelection, CoreMLEPCreateAndPublishEpDevices) { EXPECT_TRUE(released.empty()); } } - -// Verifies that OrderDevices ranks CoreML ahead of another EP for the same Apple GPU because of vendor affinity. -// CoreMLEPPreferGpu does not test this rule independently because both vendor affinity and EP-name ordering rank -// CoreML ahead of WebGPU. This test uses a competing EP whose name sorts before CoreML, so CoreML can rank first -// only because its Apple vendor identity matches the GPU. -TEST(AutoEpSelection, CoreMLVendorAffinityPrecedesEpNameOrdering) { - // The competitor's name must sort before CoreML's. Otherwise, EP-name ordering could also put CoreML first, and - // the test would not verify vendor affinity independently. - constexpr const char* kCompetitorEpName = "AaaExecutionProvider"; - ASSERT_LT(std::string_view{kCompetitorEpName}, std::string_view{kCoreMLExecutionProvider}) - << "The competitor EP name must sort before the CoreML EP name for this test to verify vendor affinity " - "independently."; - - // Both OrtEpDevices reference the same Apple GPU, so their hardware properties are identical. Only the CoreML - // factory's vendor matches the GPU, so vendor affinity determines their order. - OrtHardwareDevice apple_gpu{}; - apple_gpu.type = OrtHardwareDeviceType_GPU; - apple_gpu.vendor_id = OrtDevice::VendorIds::APPLE; - apple_gpu.vendor = "Apple"; - - OrtEpFactory apple_factory{}; - apple_factory.GetVendorId = [](const OrtEpFactory*) noexcept -> uint32_t { - return OrtDevice::VendorIds::APPLE; - }; - - OrtEpFactory competitor_factory{}; - competitor_factory.GetVendorId = [](const OrtEpFactory*) noexcept -> uint32_t { - return OrtDevice::VendorIds::MICROSOFT; - }; - - OrtEpDevice coreml_device{}; - coreml_device.ep_name = kCoreMLExecutionProvider; - coreml_device.ep_vendor = "Apple"; - coreml_device.device = &apple_gpu; - coreml_device.ep_factory = &apple_factory; - - OrtEpDevice competitor_device{}; - competitor_device.ep_name = kCompetitorEpName; - competitor_device.ep_vendor = "Microsoft"; - competitor_device.device = &apple_gpu; - competitor_device.ep_factory = &competitor_factory; - - // Test both input orders. CoreML must be placed first in both cases, confirming that vendor affinity places it - // ahead regardless of input order. - ProviderPolicyContext context; - for (const bool coreml_first : {false, true}) { - SCOPED_TRACE(testing::Message() << "coreml_first " << coreml_first); - const std::vector devices = - coreml_first ? std::vector{&coreml_device, &competitor_device} - : std::vector{&competitor_device, &coreml_device}; - - const std::vector ordered = context.OrderDevices(devices); - - ASSERT_EQ(ordered.size(), size_t{2}); - EXPECT_EQ(ordered[0], &coreml_device) - << "Vendor affinity must place CoreML ahead of a competing EP whose name sorts before CoreML's."; - EXPECT_EQ(ordered[1], &competitor_device); - } -} #endif // defined(USE_COREML) && defined(__APPLE__) #if defined(USE_COREML) && !defined(__APPLE__) diff --git a/onnxruntime/test/python/onnxruntime_test_python.py b/onnxruntime/test/python/onnxruntime_test_python.py index fd39e1b994dc8..121bbb29fa246 100644 --- a/onnxruntime/test/python/onnxruntime_test_python.py +++ b/onnxruntime/test/python/onnxruntime_test_python.py @@ -1798,8 +1798,8 @@ def test_ort_device(self): self.assertEqual(cuda_device.device_vendor_id(), onnxrt.OrtDeviceVendorId.NVIDIA) self.assertEqual(onnxrt.OrtDeviceVendorId.NVIDIA, 0x10DE) - # OrtDeviceVendorId mirrors OrtDevice::VendorIds in ortdevice.h. The CoreML factory and Apple - # hardware discovery both use VendorIds::APPLE, so the Python value must match the C++ constant. + # OrtDeviceVendorId mirrors OrtDevice::VendorIds in ortdevice.h. Apple hardware discovery uses + # VendorIds::APPLE for the devices it reports, so the Python value must match the C++ constant. self.assertEqual(onnxrt.OrtDeviceVendorId.APPLE, 0x106B) # When Apple accelerator hardware is available, also verify the vendor ID returned by get_hardware_devices(). From 38dd9eb5ef1a802d1d3d26b46ee10fb331eaede5 Mon Sep 17 00:00:00 2001 From: Maxim Davgalev Date: Tue, 25 Aug 2026 17:21:50 -0700 Subject: [PATCH 03/12] Prefix MLComputeUnits value constants with the option name Rename kCoremlComputeUnits_* to kCoremlProviderOption_MLComputeUnits_*. --- .../core/providers/coreml/coreml_provider_factory.h | 12 ++++++------ onnxruntime/core/providers/coreml/coreml_options.cc | 8 ++++---- .../core/session/plugin_ep/ep_factory_coreml.cc | 12 +++++++----- onnxruntime/test/autoep/test_selection.cc | 6 ++++-- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/include/onnxruntime/core/providers/coreml/coreml_provider_factory.h b/include/onnxruntime/core/providers/coreml/coreml_provider_factory.h index 62f57acdb84e2..071a9d264a355 100644 --- a/include/onnxruntime/core/providers/coreml/coreml_provider_factory.h +++ b/include/onnxruntime/core/providers/coreml/coreml_provider_factory.h @@ -41,17 +41,17 @@ enum COREMLFlags { COREML_FLAG_LAST = COREML_FLAG_USE_CPU_AND_GPU, }; -// Set the MLComputeUnits provider option to one of the kCoremlComputeUnits_* values below. Each value maps to -// the corresponding Core ML compute-unit mode +// Set the MLComputeUnits provider option to one of the kCoremlProviderOption_MLComputeUnits_* values below. +// Each value maps to the corresponding Core ML compute-unit mode // (https://developer.apple.com/documentation/coreml/mlcomputeunits). // Use these values with SessionOptionsAppendExecutionProvider or SessionOptionsAppendExecutionProvider_V2. // The legacy CoreML-specific OrtSessionOptionsAppendExecutionProvider_CoreML API uses a COREMLFlags bitmask // instead. // MLComputeUnitsCPUAndNeuralEngine requires Core ML 6 (iOS 16 / macOS 13) and is unavailable on earlier systems. -static const char* const kCoremlComputeUnits_CPUAndNeuralEngine = "CPUAndNeuralEngine"; -static const char* const kCoremlComputeUnits_CPUAndGPU = "CPUAndGPU"; -static const char* const kCoremlComputeUnits_CPUOnly = "CPUOnly"; -static const char* const kCoremlComputeUnits_ALL = "ALL"; +static const char* const kCoremlProviderOption_MLComputeUnits_CPUAndNeuralEngine = "CPUAndNeuralEngine"; +static const char* const kCoremlProviderOption_MLComputeUnits_CPUAndGPU = "CPUAndGPU"; +static const char* const kCoremlProviderOption_MLComputeUnits_CPUOnly = "CPUOnly"; +static const char* const kCoremlProviderOption_MLComputeUnits_ALL = "ALL"; static const char* const kCoremlProviderOption_MLComputeUnits = "MLComputeUnits"; static const char* const kCoremlProviderOption_ModelFormat = "ModelFormat"; // same as COREML_FLAG_ONLY_ALLOW_STATIC_INPUT_SHAPES diff --git a/onnxruntime/core/providers/coreml/coreml_options.cc b/onnxruntime/core/providers/coreml/coreml_options.cc index f3adf15dafd1f..daaab86be5046 100644 --- a/onnxruntime/core/providers/coreml/coreml_options.cc +++ b/onnxruntime/core/providers/coreml/coreml_options.cc @@ -43,10 +43,10 @@ CoreMLOptions::CoreMLOptions(uint32_t coreml_flags) { void CoreMLOptions::ValidateAndParseProviderOption(const ProviderOptions& options) { const std::unordered_map available_computeunits_options = { - {kCoremlComputeUnits_CPUAndNeuralEngine, COREML_FLAG_ONLY_ENABLE_DEVICE_WITH_ANE}, - {kCoremlComputeUnits_CPUAndGPU, COREML_FLAG_USE_CPU_AND_GPU}, - {kCoremlComputeUnits_CPUOnly, COREML_FLAG_USE_CPU_ONLY}, - {kCoremlComputeUnits_ALL, COREML_FLAG_USE_NONE}, + {kCoremlProviderOption_MLComputeUnits_CPUAndNeuralEngine, COREML_FLAG_ONLY_ENABLE_DEVICE_WITH_ANE}, + {kCoremlProviderOption_MLComputeUnits_CPUAndGPU, COREML_FLAG_USE_CPU_AND_GPU}, + {kCoremlProviderOption_MLComputeUnits_CPUOnly, COREML_FLAG_USE_CPU_ONLY}, + {kCoremlProviderOption_MLComputeUnits_ALL, COREML_FLAG_USE_NONE}, }; const std::unordered_map available_modelformat_options = { {"MLProgram", COREML_FLAG_CREATE_MLPROGRAM}, diff --git a/onnxruntime/core/session/plugin_ep/ep_factory_coreml.cc b/onnxruntime/core/session/plugin_ep/ep_factory_coreml.cc index 6fdbddf26b9f9..d2f6cc205e94e 100644 --- a/onnxruntime/core/session/plugin_ep/ep_factory_coreml.cc +++ b/onnxruntime/core/session/plugin_ep/ep_factory_coreml.cc @@ -213,9 +213,9 @@ OrtStatus* CoreMLEpFactory::ValidateDeviceSelectionAndResolveMLComputeUnits( const auto compute_units_it = options.find(kCoremlProviderOption_MLComputeUnits); if (compute_units_it == options.end()) { options[kCoremlProviderOption_MLComputeUnits] = - (num_npu > 0 && num_gpu > 0) ? kCoremlComputeUnits_ALL - : num_npu > 0 ? kCoremlComputeUnits_CPUAndNeuralEngine - : kCoremlComputeUnits_CPUAndGPU; + (num_npu > 0 && num_gpu > 0) ? kCoremlProviderOption_MLComputeUnits_ALL + : num_npu > 0 ? kCoremlProviderOption_MLComputeUnits_CPUAndNeuralEngine + : kCoremlProviderOption_MLComputeUnits_CPUAndGPU; return nullptr; } @@ -226,9 +226,11 @@ OrtStatus* CoreMLEpFactory::ValidateDeviceSelectionAndResolveMLComputeUnits( // the classification below accordingly. const std::string& compute_units = compute_units_it->second; const bool enables_npu = - compute_units == kCoremlComputeUnits_CPUAndNeuralEngine || compute_units == kCoremlComputeUnits_ALL; + compute_units == kCoremlProviderOption_MLComputeUnits_CPUAndNeuralEngine || + compute_units == kCoremlProviderOption_MLComputeUnits_ALL; const bool enables_gpu = - compute_units == kCoremlComputeUnits_CPUAndGPU || compute_units == kCoremlComputeUnits_ALL; + compute_units == kCoremlProviderOption_MLComputeUnits_CPUAndGPU || + compute_units == kCoremlProviderOption_MLComputeUnits_ALL; if ((enables_npu && num_npu == 0) || (enables_gpu && num_gpu == 0)) { const std::string message = MakeString("MLComputeUnits value '", compute_units, diff --git a/onnxruntime/test/autoep/test_selection.cc b/onnxruntime/test/autoep/test_selection.cc index 4f27676393851..218811fe815f4 100644 --- a/onnxruntime/test/autoep/test_selection.cc +++ b/onnxruntime/test/autoep/test_selection.cc @@ -521,7 +521,8 @@ TEST(AutoEpSelection, CoreMLEPMultipleDevicesBothReachTheFactory) { ep_devices.emplace_back(coreml_devices.npu); ep_devices.emplace_back(coreml_devices.gpu); - for (const char* compute_units : {kCoremlComputeUnits_CPUAndGPU, kCoremlComputeUnits_CPUAndNeuralEngine}) { + for (const char* compute_units : {kCoremlProviderOption_MLComputeUnits_CPUAndGPU, + kCoremlProviderOption_MLComputeUnits_CPUAndNeuralEngine}) { SCOPED_TRACE(compute_units); Ort::KeyValuePairs ep_options; @@ -643,7 +644,8 @@ TEST(AutoEpSelection, CoreMLLegacySessionReportsFirstAdvertisedEpDevice) { Ort::SessionOptions session_options; session_options.AppendExecutionProvider( - kCoreMLExecutionProvider, {{kCoremlProviderOption_MLComputeUnits, kCoremlComputeUnits_CPUOnly}}); + kCoreMLExecutionProvider, + {{kCoremlProviderOption_MLComputeUnits, kCoremlProviderOption_MLComputeUnits_CPUOnly}}); Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); const OrtApi* c_api = &Ort::GetApi(); From 0482abeded1c88aa65b56d397e13d13688657516 Mon Sep 17 00:00:00 2001 From: Maxim Davgalev Date: Thu, 27 Aug 2026 18:59:19 -0700 Subject: [PATCH 04/12] Fix python test to call get_hardware_devices via _pybind_state Call the native binding through the _pybind_state module. --- onnxruntime/test/python/onnxruntime_test_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onnxruntime/test/python/onnxruntime_test_python.py b/onnxruntime/test/python/onnxruntime_test_python.py index 121bbb29fa246..5af0f1d5474e9 100644 --- a/onnxruntime/test/python/onnxruntime_test_python.py +++ b/onnxruntime/test/python/onnxruntime_test_python.py @@ -1806,7 +1806,7 @@ def test_ort_device(self): apple_accelerator = next( ( device - for device in onnxrt.get_hardware_devices() + for device in C.get_hardware_devices() if device.type in (onnxrt.OrtHardwareDeviceType.GPU, onnxrt.OrtHardwareDeviceType.NPU) and device.vendor == "Apple" ), From 7df72444a218d811eda48e8ecaa4b9a7deb05468 Mon Sep 17 00:00:00 2001 From: Maxim Davgalev Date: Fri, 28 Aug 2026 18:54:55 -0700 Subject: [PATCH 05/12] Remove OrtDevice::VendorIds::APPLE After the CoreML factory switched to the Microsoft vendor ID, OrtDevice::VendorIds::APPLE was no longer used. Apple device discovery keeps its local kApplePciVendorId constant (0x106B) as before. --- include/onnxruntime/core/framework/ortdevice.h | 1 - .../core/platform/apple/device_discovery.cc | 3 +-- .../core/session/plugin_ep/ep_factory_coreml.h | 2 +- .../python/onnxruntime_inference_collection.py | 1 - onnxruntime/test/autoep/test_selection.cc | 8 ++++---- .../test/python/onnxruntime_test_python.py | 17 ----------------- 6 files changed, 6 insertions(+), 26 deletions(-) diff --git a/include/onnxruntime/core/framework/ortdevice.h b/include/onnxruntime/core/framework/ortdevice.h index afba978ee022f..c85b01210fc3b 100644 --- a/include/onnxruntime/core/framework/ortdevice.h +++ b/include/onnxruntime/core/framework/ortdevice.h @@ -57,7 +57,6 @@ struct OrtDevice { // No vendor ID. Valid for DeviceType::CPU + MemType::DEFAULT or for generic allocators like WebGPU. NONE = 0x0000, AMD = 0x1002, // MIGraphX EP - APPLE = 0x106B, // Apple hardware via device discovery NVIDIA = 0x10DE, // CUDA/TensorRT ARM = 0x13B5, // ARM GPU EP MICROSOFT = 0x1414, // DML EP diff --git a/onnxruntime/core/platform/apple/device_discovery.cc b/onnxruntime/core/platform/apple/device_discovery.cc index addaa419f7ef7..767b834e38756 100644 --- a/onnxruntime/core/platform/apple/device_discovery.cc +++ b/onnxruntime/core/platform/apple/device_discovery.cc @@ -7,13 +7,12 @@ #include #include "core/common/logging/logging.h" -#include "core/framework/ortdevice.h" namespace onnxruntime { namespace { -constexpr auto kApplePciVendorId = OrtDevice::VendorIds::APPLE; +constexpr auto kApplePciVendorId = 0x106B; constexpr auto kAppleVendorName = "Apple"; std::vector GetGpuDevices() { diff --git a/onnxruntime/core/session/plugin_ep/ep_factory_coreml.h b/onnxruntime/core/session/plugin_ep/ep_factory_coreml.h index 15daddffc7abd..df2164a64ec39 100644 --- a/onnxruntime/core/session/plugin_ep/ep_factory_coreml.h +++ b/onnxruntime/core/session/plugin_ep/ep_factory_coreml.h @@ -28,7 +28,7 @@ class CoreMLEpFactory : public EpFactoryInternalImpl { public: // Report "Microsoft" and VendorIds::MICROSOFT as the factory vendor identifiers, like the other internal // factories (CPU, DML, and WebGPU): the factory vendor identifies who provides the EP implementation, not the - // hardware vendor. Discovered Apple hardware devices keep their own "Apple" / VendorIds::APPLE identifiers. + // hardware vendor. Discovered Apple hardware devices keep their own Apple vendor identifiers. // Because the factory vendor does not match the Apple hardware vendor, device ordering gives CoreML no vendor // affinity. When multiple EPs target the same NPU or GPU with equal vendor affinity, they are ordered by // EP name. diff --git a/onnxruntime/python/onnxruntime_inference_collection.py b/onnxruntime/python/onnxruntime_inference_collection.py index 6cbf681cb26bc..70f699768e92c 100644 --- a/onnxruntime/python/onnxruntime_inference_collection.py +++ b/onnxruntime/python/onnxruntime_inference_collection.py @@ -45,7 +45,6 @@ class OrtDeviceVendorId(IntEnum): NONE = 0x0000 AMD = 0x1002 - APPLE = 0x106B NVIDIA = 0x10DE ARM = 0x13B5 MICROSOFT = 0x1414 diff --git a/onnxruntime/test/autoep/test_selection.cc b/onnxruntime/test/autoep/test_selection.cc index 218811fe815f4..5271d23eae81f 100644 --- a/onnxruntime/test/autoep/test_selection.cc +++ b/onnxruntime/test/autoep/test_selection.cc @@ -448,9 +448,9 @@ TEST(AutoEpSelection, CoreMLEP) { // Verify the CoreML factory's vendor name and vendor ID, along with the hardware vendor ID. The factory reports // "Microsoft", like the other internal factories: the factory vendor identifies who provides the EP // implementation, not the hardware vendor. - // Apple device discovery currently assigns VendorIds::APPLE to every NPU and GPU it reports. The CoreML factory - // intentionally accepts any discovered GPU regardless of vendor, so this hardware vendor ID assertion must be - // updated if discovery later reports a non-Apple GPU (see the multi-GPU TODO in + // Apple device discovery currently assigns Apple's PCI vendor id (0x106B) to every NPU and GPU it reports. The + // CoreML factory intentionally accepts any discovered GPU regardless of vendor, so this hardware vendor ID + // assertion must be updated if discovery later reports a non-Apple GPU (see the multi-GPU TODO in // core/platform/apple/device_discovery.cc). const OrtApi* c_api = &Ort::GetApi(); size_t num_ep_devices = 0; @@ -464,7 +464,7 @@ TEST(AutoEpSelection, CoreMLEP) { } EXPECT_STREQ(c_api->EpDevice_EpVendor(ep_device), "Microsoft"); - EXPECT_EQ(c_api->HardwareDevice_VendorId(c_api->EpDevice_Device(ep_device)), OrtDevice::VendorIds::APPLE); + EXPECT_EQ(c_api->HardwareDevice_VendorId(c_api->EpDevice_Device(ep_device)), uint32_t{0x106B}); EXPECT_EQ(ep_device->ep_factory->GetVendorId(ep_device->ep_factory), OrtDevice::VendorIds::MICROSOFT); } diff --git a/onnxruntime/test/python/onnxruntime_test_python.py b/onnxruntime/test/python/onnxruntime_test_python.py index 5af0f1d5474e9..d60acfc8bc34f 100644 --- a/onnxruntime/test/python/onnxruntime_test_python.py +++ b/onnxruntime/test/python/onnxruntime_test_python.py @@ -1798,23 +1798,6 @@ def test_ort_device(self): self.assertEqual(cuda_device.device_vendor_id(), onnxrt.OrtDeviceVendorId.NVIDIA) self.assertEqual(onnxrt.OrtDeviceVendorId.NVIDIA, 0x10DE) - # OrtDeviceVendorId mirrors OrtDevice::VendorIds in ortdevice.h. Apple hardware discovery uses - # VendorIds::APPLE for the devices it reports, so the Python value must match the C++ constant. - self.assertEqual(onnxrt.OrtDeviceVendorId.APPLE, 0x106B) - - # When Apple accelerator hardware is available, also verify the vendor ID returned by get_hardware_devices(). - apple_accelerator = next( - ( - device - for device in C.get_hardware_devices() - if device.type in (onnxrt.OrtHardwareDeviceType.GPU, onnxrt.OrtHardwareDeviceType.NPU) - and device.vendor == "Apple" - ), - None, - ) - if apple_accelerator is not None: - self.assertEqual(apple_accelerator.vendor_id, onnxrt.OrtDeviceVendorId.APPLE) - def test_ort_memory_info(self): cpu_memory_info = onnxrt.OrtMemoryInfo( "Cpu", From 2cb0bcc8dc5b8b85ae516bd67a6b75c66d1a80ff Mon Sep 17 00:00:00 2001 From: Maxim Davgalev Date: Fri, 4 Sep 2026 19:23:49 -0700 Subject: [PATCH 06/12] Remove try/catch from the CoreML EP factory entry points The CoreML EP factory is an internal EP: there is no library boundary between it and ORT, so its entry points do not need to convert exceptions into OrtStatus. --- .../session/plugin_ep/ep_factory_coreml.cc | 81 +++++++------------ .../session/plugin_ep/ep_factory_coreml.h | 2 +- 2 files changed, 29 insertions(+), 54 deletions(-) diff --git a/onnxruntime/core/session/plugin_ep/ep_factory_coreml.cc b/onnxruntime/core/session/plugin_ep/ep_factory_coreml.cc index d2f6cc205e94e..438f36257a690 100644 --- a/onnxruntime/core/session/plugin_ep/ep_factory_coreml.cc +++ b/onnxruntime/core/session/plugin_ep/ep_factory_coreml.cc @@ -98,40 +98,23 @@ OrtStatus* CoreMLEpFactory::GetSupportedDevices(EpFactoryInternal& ep_factory, OrtEpDevice** ep_devices, size_t max_ep_devices, size_t* p_num_ep_devices) noexcept { - OrtStatus* status = nullptr; - - ORT_TRY { - const auto selected = - SelectDevicesToClaim(gsl::make_span(devices, num_devices), coreml::util::CoreMLVersion(), max_ep_devices); - - // Do not attach MLComputeUnits to individual OrtEpDevice instances. Per-device defaults would assign different - // values to the NPU and GPU, creating a conflict when both are selected. CreateIExecutionProvider instead computes - // a single value from all selected devices. Any MLComputeUnits value already present in the session options was - // provided by the caller and is preserved if it is compatible with the selection. - const auto create_ep_device = - [&ep_factory](const OrtHardwareDevice& device, OrtEpDevice** ep_device) -> OrtStatus* { - return OrtExecutionProviderApi::CreateEpDevice(&ep_factory, &device, nullptr, nullptr, ep_device); - }; - - const auto release_ep_device = [](OrtEpDevice* ep_device) { - OrtExecutionProviderApi::ReleaseEpDevice(ep_device); - }; - - status = CreateAndPublishEpDevices( - selected, - create_ep_device, - release_ep_device, - ep_devices, p_num_ep_devices); - } - ORT_CATCH(const std::exception& ex) { - // Convert local allocation failures to OrtStatus because this method is noexcept. Device selection and storage - // reservation finish before CreateEpDevice is called, so this exception path cannot leak an OrtEpDevice. - ORT_HANDLE_EXCEPTION([&]() { - status = OrtApis::CreateStatus(ORT_FAIL, ex.what()); - }); - } - - return status; + const auto selected = + SelectDevicesToClaim(gsl::make_span(devices, num_devices), coreml::util::CoreMLVersion(), max_ep_devices); + + // Do not attach MLComputeUnits to individual OrtEpDevice instances. Per-device defaults would assign different + // values to the NPU and GPU, creating a conflict when both are selected. CreateIExecutionProvider instead computes + // a single value from all selected devices. Any MLComputeUnits value already present in the session options was + // provided by the caller and is preserved if it is compatible with the selection. + const auto create_ep_device = + [&ep_factory](const OrtHardwareDevice& device, OrtEpDevice** ep_device) -> OrtStatus* { + return OrtExecutionProviderApi::CreateEpDevice(&ep_factory, &device, nullptr, nullptr, ep_device); + }; + + const auto release_ep_device = [](OrtEpDevice* ep_device) { + OrtExecutionProviderApi::ReleaseEpDevice(ep_device); + }; + + return CreateAndPublishEpDevices(selected, create_ep_device, release_ep_device, ep_devices, p_num_ep_devices); } OrtStatus* CoreMLEpFactory::CreateIExecutionProvider(const OrtHardwareDevice* const* devices, @@ -139,30 +122,22 @@ OrtStatus* CoreMLEpFactory::CreateIExecutionProvider(const OrtHardwareDevice* co size_t num_devices, const OrtSessionOptions* session_options, const OrtLogger* session_logger, - std::unique_ptr* ep) noexcept { - OrtStatus* status = nullptr; + std::unique_ptr* ep) { *ep = nullptr; - ORT_TRY { - ProviderOptions options = GetOptionsFromSessionOptions(session_options->value); + ProviderOptions options = GetOptionsFromSessionOptions(session_options->value); - // OrtEpDevice instances carry no MLComputeUnits default, so any existing session option can only come from - // the caller. Validate the device selection, derive MLComputeUnits when absent, and reject recognized values - // that enable an unselected accelerator. - // CoreMLOptions performs the remaining option validation during provider creation. - ORT_API_RETURN_IF_ERROR(ValidateDeviceSelectionAndResolveMLComputeUnits(devices, num_devices, options)); + // OrtEpDevice instances carry no MLComputeUnits default, so any existing session option can only come from + // the caller. Validate the device selection, derive MLComputeUnits when absent, and reject recognized values + // that enable an unselected accelerator. + // CoreMLOptions performs the remaining option validation during provider creation. + ORT_API_RETURN_IF_ERROR(ValidateDeviceSelectionAndResolveMLComputeUnits(devices, num_devices, options)); - const auto provider_factory = CoreMLProviderFactoryCreator::Create(options); - *ep = provider_factory->CreateProvider(); - (*ep)->SetLogger(session_logger->ToInternal()); - } - ORT_CATCH(const std::exception& ex) { - ORT_HANDLE_EXCEPTION([&]() { - status = OrtApis::CreateStatus(ORT_FAIL, ex.what()); - }); - } + const auto provider_factory = CoreMLProviderFactoryCreator::Create(options); + *ep = provider_factory->CreateProvider(); + (*ep)->SetLogger(session_logger->ToInternal()); - return status; + return nullptr; } OrtStatus* CoreMLEpFactory::ValidateDeviceSelectionAndResolveMLComputeUnits( diff --git a/onnxruntime/core/session/plugin_ep/ep_factory_coreml.h b/onnxruntime/core/session/plugin_ep/ep_factory_coreml.h index df2164a64ec39..49a88fac8cf15 100644 --- a/onnxruntime/core/session/plugin_ep/ep_factory_coreml.h +++ b/onnxruntime/core/session/plugin_ep/ep_factory_coreml.h @@ -116,7 +116,7 @@ class CoreMLEpFactory : public EpFactoryInternalImpl { size_t num_devices, const OrtSessionOptions* session_options, const OrtLogger* session_logger, - std::unique_ptr* ep) noexcept override; + std::unique_ptr* ep) override; }; } // namespace onnxruntime From e8089b21f86ef89500f44570963188c023a540c1 Mon Sep 17 00:00:00 2001 From: Maxim Davgalev Date: Mon, 7 Sep 2026 22:55:09 -0700 Subject: [PATCH 07/12] Make the CoreML EP factory helpers file-scoped and trim its tests The device claiming and option validation helpers of CoreMLEpFactory were public static members only so that unit tests could call them directly. Move them into an anonymous namespace in ep_factory_coreml.cc, fold OrtEpDevice creation and publication into GetSupportedDevices, and remove the tests that called the helpers directly. --- .../session/plugin_ep/ep_factory_coreml.cc | 181 ++-- .../session/plugin_ep/ep_factory_coreml.h | 72 -- onnxruntime/test/autoep/test_selection.cc | 782 ++++++------------ 3 files changed, 384 insertions(+), 651 deletions(-) diff --git a/onnxruntime/core/session/plugin_ep/ep_factory_coreml.cc b/onnxruntime/core/session/plugin_ep/ep_factory_coreml.cc index 438f36257a690..1c6f678067578 100644 --- a/onnxruntime/core/session/plugin_ep/ep_factory_coreml.cc +++ b/onnxruntime/core/session/plugin_ep/ep_factory_coreml.cc @@ -10,8 +10,14 @@ #include "core/session/plugin_ep/ep_factory_coreml.h" +#include +#include + +#include + #include "core/common/common.h" #include "core/framework/error_code_helper.h" +#include "core/framework/provider_options.h" #include "core/providers/coreml/coreml_provider_factory.h" #include "core/providers/coreml/coreml_provider_factory_creator.h" #include "core/providers/coreml/model/host_utils.h" @@ -23,12 +29,14 @@ #include "core/session/ort_apis.h" namespace onnxruntime { - -bool CoreMLEpFactory::CanClaimDeviceType(OrtHardwareDeviceType device_type, int32_t coreml_version) { - // The factory advertises no devices below Core ML 5 (macOS 12 / iOS 15), the minimum version supported by - // the current CoreML EP. Although earlier Core ML versions can use accelerators, CoreMLExecutionProvider - // rejects those systems during construction. Advertising a device on those systems would cause session creation - // to fail after selection instead of allowing the policy to select another EP. +namespace { + +// Returns whether the factory advertises an OrtEpDevice for the given hardware device type and Core ML version. +// Nothing is advertised below Core ML 5 (MINIMUM_COREML_VERSION), because CoreMLExecutionProvider rejects earlier +// versions during construction. Advertising a device there would make provider creation fail after selection +// instead of allowing the policy to select another EP. NPU devices additionally require +// MINIMUM_COREML_VERSION_FOR_NEURAL_ENGINE_SELECTION. CPU devices are never advertised. +bool CanClaimDeviceType(OrtHardwareDeviceType device_type, int32_t coreml_version) { if (coreml_version < MINIMUM_COREML_VERSION) { return false; } @@ -57,13 +65,29 @@ bool CoreMLEpFactory::CanClaimDeviceType(OrtHardwareDeviceType device_type, int3 } } -InlinedVector CoreMLEpFactory::SelectDevicesToClaim( - gsl::span devices, int32_t coreml_version, size_t max_ep_devices) { - InlinedVector selected; +// The factory advertises at most one NPU and one GPU (see SelectDevicesToClaim), so pointers to both the selected +// hardware devices and the newly created OrtEpDevice instances fit in fixed-size arrays. GetSupportedDevices +// therefore needs no dynamic allocation for local storage. CreateEpDevice reports failures through OrtStatus, +// so GetSupportedDevices satisfies the base interface's noexcept requirement. +constexpr size_t kMaxClaimedDevices = 2; + +struct ClaimedDevices { + std::array devices{}; + size_t count = 0; +}; + +// Selects the hardware devices to advertise. The result contains only devices accepted by CanClaimDeviceType, +// at most one device of each type, and no more than max_ep_devices devices in total. +// MLComputeUnits selects device classes rather than specific physical devices, so CoreML cannot target a particular +// GPU or NPU when multiple devices of that type exist. Apple device discovery currently reports at most one device +// of each type. Additional same type devices are intentionally ignored. +ClaimedDevices SelectDevicesToClaim(gsl::span devices, int32_t coreml_version, + size_t max_ep_devices) { + ClaimedDevices selected; bool npu_claimed = false; bool gpu_claimed = false; - for (size_t i = 0; i < devices.size() && selected.size() < max_ep_devices; ++i) { + for (size_t i = 0; i < devices.size() && selected.count < max_ep_devices; ++i) { const OrtHardwareDevice* device = devices[i]; // CanClaimDeviceType checks the Core ML version and excludes CPU devices. @@ -71,12 +95,9 @@ InlinedVector CoreMLEpFactory::SelectDevicesToClaim continue; } - // Core ML selects compute-unit classes, not individual physical devices, so advertise at most one NPU and one GPU. - // Apple device discovery currently reports at most one device of each type. If it later reports several devices - // of the same type, this function keeps the first one. Additional devices are intentionally ignored because - // Core ML cannot target a specific physical device. - // CanClaimDeviceType currently accepts only NPU and GPU devices, so this logic treats every accepted non-NPU - // device as a GPU. If support for another device type is added, update this selection logic as well. + // CanClaimDeviceType accepts only NPU and GPU devices, so every accepted non-NPU device is a GPU. + // Only the first device of each type is selected. If CanClaimDeviceType is extended to accept another device type, + // update this selection logic as well. const bool is_npu = device->type == OrtHardwareDeviceType::OrtHardwareDeviceType_NPU; bool& claimed = is_npu ? npu_claimed : gpu_claimed; @@ -86,62 +107,21 @@ InlinedVector CoreMLEpFactory::SelectDevicesToClaim claimed = true; - selected.push_back(device); + selected.devices[selected.count++] = device; } return selected; } -OrtStatus* CoreMLEpFactory::GetSupportedDevices(EpFactoryInternal& ep_factory, - const OrtHardwareDevice* const* devices, - size_t num_devices, - OrtEpDevice** ep_devices, - size_t max_ep_devices, - size_t* p_num_ep_devices) noexcept { - const auto selected = - SelectDevicesToClaim(gsl::make_span(devices, num_devices), coreml::util::CoreMLVersion(), max_ep_devices); - - // Do not attach MLComputeUnits to individual OrtEpDevice instances. Per-device defaults would assign different - // values to the NPU and GPU, creating a conflict when both are selected. CreateIExecutionProvider instead computes - // a single value from all selected devices. Any MLComputeUnits value already present in the session options was - // provided by the caller and is preserved if it is compatible with the selection. - const auto create_ep_device = - [&ep_factory](const OrtHardwareDevice& device, OrtEpDevice** ep_device) -> OrtStatus* { - return OrtExecutionProviderApi::CreateEpDevice(&ep_factory, &device, nullptr, nullptr, ep_device); - }; - - const auto release_ep_device = [](OrtEpDevice* ep_device) { - OrtExecutionProviderApi::ReleaseEpDevice(ep_device); - }; - - return CreateAndPublishEpDevices(selected, create_ep_device, release_ep_device, ep_devices, p_num_ep_devices); -} - -OrtStatus* CoreMLEpFactory::CreateIExecutionProvider(const OrtHardwareDevice* const* devices, - const OrtKeyValuePairs* const* /*ep_metadata_pairs*/, - size_t num_devices, - const OrtSessionOptions* session_options, - const OrtLogger* session_logger, - std::unique_ptr* ep) { - *ep = nullptr; - - ProviderOptions options = GetOptionsFromSessionOptions(session_options->value); - - // OrtEpDevice instances carry no MLComputeUnits default, so any existing session option can only come from - // the caller. Validate the device selection, derive MLComputeUnits when absent, and reject recognized values - // that enable an unselected accelerator. - // CoreMLOptions performs the remaining option validation during provider creation. - ORT_API_RETURN_IF_ERROR(ValidateDeviceSelectionAndResolveMLComputeUnits(devices, num_devices, options)); - - const auto provider_factory = CoreMLProviderFactoryCreator::Create(options); - *ep = provider_factory->CreateProvider(); - (*ep)->SetLogger(session_logger->ToInternal()); - - return nullptr; -} - -OrtStatus* CoreMLEpFactory::ValidateDeviceSelectionAndResolveMLComputeUnits( - const OrtHardwareDevice* const* devices, size_t num_devices, ProviderOptions& options) { +// Validates that 'devices' contains exactly one NPU, one GPU, or one NPU plus one GPU. Otherwise, returns +// ORT_INVALID_ARGUMENT. The CPU device is not valid because the factory does not advertise it. +// If 'options' does not contain MLComputeUnits, derives a default from the selection: +// CPUAndNeuralEngine for NPU, CPUAndGPU for GPU, and ALL for NPU plus GPU. +// A caller-provided value may disable selected accelerators, as CPUOnly does, but it must not enable an +// accelerator that was not selected. Unrecognized values are left for CoreMLOptions to validate during +// provider creation. +OrtStatus* ValidateDeviceSelectionAndResolveMLComputeUnits(const OrtHardwareDevice* const* devices, + size_t num_devices, ProviderOptions& options) { if (num_devices == 0) { return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, "CoreML EP factory requires at least one device to be selected."); @@ -217,6 +197,73 @@ OrtStatus* CoreMLEpFactory::ValidateDeviceSelectionAndResolveMLComputeUnits( return nullptr; } +} // namespace + +OrtStatus* CoreMLEpFactory::GetSupportedDevices(EpFactoryInternal& ep_factory, + const OrtHardwareDevice* const* devices, + size_t num_devices, + OrtEpDevice** ep_devices, + size_t max_ep_devices, + size_t* p_num_ep_devices) noexcept { + const ClaimedDevices selected = + SelectDevicesToClaim(gsl::make_span(devices, num_devices), coreml::util::CoreMLVersion(), max_ep_devices); + + // Do not attach MLComputeUnits to individual OrtEpDevice instances. Per-device defaults would assign different + // values to the NPU and GPU, creating a conflict when both are selected. CreateIExecutionProvider instead computes + // a single value from all selected devices. Any MLComputeUnits value already present in the session options was + // provided by the caller. + // + // Publish the OrtEpDevice instances only after all creations succeed. If a creation fails, release the devices + // created by earlier calls and leave ep_devices and p_num_ep_devices unchanged, following ORT's C API convention + // for failed calls. 'selected' contains no more than max_ep_devices entries, so publishing cannot exceed the + // capacity of ep_devices. + std::array created{}; + size_t num_created = 0; + + for (size_t i = 0; i < selected.count; ++i) { + OrtEpDevice* ep_device = nullptr; + OrtStatus* status = + OrtExecutionProviderApi::CreateEpDevice(&ep_factory, selected.devices[i], nullptr, nullptr, &ep_device); + if (status != nullptr) { + for (size_t j = 0; j < num_created; ++j) { + OrtExecutionProviderApi::ReleaseEpDevice(created[j]); + } + + return status; + } + + created[num_created++] = ep_device; + } + + std::copy_n(created.begin(), num_created, ep_devices); + *p_num_ep_devices = num_created; + + return nullptr; +} + +OrtStatus* CoreMLEpFactory::CreateIExecutionProvider(const OrtHardwareDevice* const* devices, + const OrtKeyValuePairs* const* /*ep_metadata_pairs*/, + size_t num_devices, + const OrtSessionOptions* session_options, + const OrtLogger* session_logger, + std::unique_ptr* ep) { + *ep = nullptr; + + ProviderOptions options = GetOptionsFromSessionOptions(session_options->value); + + // OrtEpDevice instances carry no MLComputeUnits default, so any existing session option can only come from + // the caller. Validate the device selection, derive MLComputeUnits when absent, and reject recognized values + // that enable an unselected accelerator. + // CoreMLOptions performs the remaining option validation during provider creation. + ORT_API_RETURN_IF_ERROR(ValidateDeviceSelectionAndResolveMLComputeUnits(devices, num_devices, options)); + + const auto provider_factory = CoreMLProviderFactoryCreator::Create(options); + *ep = provider_factory->CreateProvider(); + (*ep)->SetLogger(session_logger->ToInternal()); + + return nullptr; +} + } // namespace onnxruntime #endif // defined(USE_COREML) && defined(__APPLE__) diff --git a/onnxruntime/core/session/plugin_ep/ep_factory_coreml.h b/onnxruntime/core/session/plugin_ep/ep_factory_coreml.h index 49a88fac8cf15..74271427ff0b9 100644 --- a/onnxruntime/core/session/plugin_ep/ep_factory_coreml.h +++ b/onnxruntime/core/session/plugin_ep/ep_factory_coreml.h @@ -12,14 +12,8 @@ #include "core/session/plugin_ep/ep_factory_internal_impl.h" -#include - -#include - #include "core/common/common.h" -#include "core/common/inlined_containers.h" #include "core/framework/ortdevice.h" -#include "core/framework/provider_options.h" #include "core/graph/constants.h" namespace onnxruntime { @@ -37,72 +31,6 @@ class CoreMLEpFactory : public EpFactoryInternalImpl { ORT_DISALLOW_COPY_ASSIGNMENT_AND_MOVE(CoreMLEpFactory); - // Validates that 'devices' contains exactly one NPU, one GPU, or one NPU plus one GPU. Otherwise, returns - // ORT_INVALID_ARGUMENT. The CPU device is never part of a valid selection as the factory does not claim it. - // If 'options' does not contain MLComputeUnits, derives the default from the selection: - // CPUAndNeuralEngine for NPU, CPUAndGPU for GPU, and ALL for NPU plus GPU. - // A caller-provided value may disable selected accelerators, as CPUOnly does, but it must not enable - // an accelerator that was not selected. - // Unrecognized values are left for CoreMLOptions to validate during provider creation. - static OrtStatus* ValidateDeviceSelectionAndResolveMLComputeUnits(const OrtHardwareDevice* const* devices, - size_t num_devices, - ProviderOptions& options); - - // Returns whether the factory advertises an OrtEpDevice for a hardware device of this type at this Core ML - // version. No devices are advertised below Core ML 5 (MINIMUM_COREML_VERSION, the EP's minimum). NPU devices - // additionally require MINIMUM_COREML_VERSION_FOR_NEURAL_ENGINE_SELECTION. CPU devices are never claimed. - static bool CanClaimDeviceType(OrtHardwareDeviceType device_type, int32_t coreml_version); - - // Selects the hardware devices for the factory to advertise. The result contains only devices accepted by - // CanClaimDeviceType, at most one device of each type, and no more than max_ep_devices in total. - // Core ML's MLComputeUnits selects a device class, not a specific physical device, so the CoreML EP cannot - // honor selection of a particular GPU or NPU when multiple devices of that type exist. Apple device discovery - // currently reports at most one device of each type. Additional same-type devices are intentionally ignored. - static InlinedVector SelectDevicesToClaim( - gsl::span devices, int32_t coreml_version, size_t max_ep_devices); - - // Creates one OrtEpDevice for each selected hardware device. The results are published only after all creations - // succeed. If a creation fails, the function releases all devices created by earlier calls and leaves ep_devices - // and p_num_ep_devices unchanged, following ORT's C API convention for failed calls. A successful call always - // writes the count, including zero when no devices were selected. - // GetSupportedDevices supplies callbacks for CreateEpDevice and ReleaseEpDevice. On success, create_ep_device must - // return nullptr and store a valid OrtEpDevice pointer in its output parameter. On failure, it must return an error - // status without modifying the output parameter. - // Neither callback may throw: create_ep_device must report failures through its returned status, and - // release_ep_device must complete cleanup for every device passed to it. - // selected.size() must not exceed the number of entries available in ep_devices. GetSupportedDevices ensures this - // by limiting SelectDevicesToClaim to max_ep_devices. - template - static OrtStatus* CreateAndPublishEpDevices(gsl::span selected, - const CreateEpDeviceFn& create_ep_device, - const ReleaseEpDeviceFn& release_ep_device, - OrtEpDevice** ep_devices, - size_t* p_num_ep_devices) { - InlinedVector created; - // Reserve before creating any devices. When DISABLE_ABSEIL maps InlinedVector to std::vector, this ensures that - // push_back cannot allocate after a device has been created. - created.reserve(selected.size()); - - for (const OrtHardwareDevice* device : selected) { - OrtEpDevice* ep_device = nullptr; - OrtStatus* status = create_ep_device(*device, &ep_device); - if (status != nullptr) { - for (OrtEpDevice* created_device : created) { - release_ep_device(created_device); - } - - return status; - } - - created.push_back(ep_device); - } - - std::copy(created.begin(), created.end(), ep_devices); - *p_num_ep_devices = created.size(); - - return nullptr; - } - private: OrtStatus* GetSupportedDevices(EpFactoryInternal& ep_factory, const OrtHardwareDevice* const* devices, diff --git a/onnxruntime/test/autoep/test_selection.cc b/onnxruntime/test/autoep/test_selection.cc index 5271d23eae81f..f3096ac76e624 100644 --- a/onnxruntime/test/autoep/test_selection.cc +++ b/onnxruntime/test/autoep/test_selection.cc @@ -3,13 +3,11 @@ #include #include -#include -#include // #include #include +#include #include -#include "core/framework/provider_options.h" #include "core/graph/constants.h" #include "core/session/abi_devices.h" #include "core/session/abi_key_value_pairs.h" @@ -18,13 +16,15 @@ #include "core/session/onnxruntime_ep_device_ep_metadata_keys.h" #include "core/session/onnxruntime_session_options_config_keys.h" -#if defined(USE_COREML) && defined(__APPLE__) -#include - +#if defined(USE_COREML) #include "core/common/inlined_containers.h" +#endif + +#if defined(USE_COREML) && defined(__APPLE__) #include "core/providers/coreml/coreml_provider_factory.h" #include "core/providers/coreml/model/host_utils.h" #include "core/session/plugin_ep/ep_factory_coreml.h" +#include "core/session/plugin_ep/ep_factory_internal.h" #endif #include "test_allocator.h" @@ -359,51 +359,29 @@ CoreMLDevices GetCoreMLEpDevices() { return found; } -// Finds the first CoreML OrtEpDevice returned by GetEpDevices, or nullptr if none is advertised. -const OrtEpDevice* GetFirstCoreMLEpDevice() { - const OrtApi* c_api = &Ort::GetApi(); - const OrtEpDevice* const* ep_devices = nullptr; - size_t num_devices = 0; - - Ort::ThrowOnError(c_api->GetEpDevices(*ort_env, &ep_devices, &num_devices)); - - for (size_t i = 0; i < num_devices; ++i) { - if (strcmp(c_api->EpDevice_EpName(ep_devices[i]), kCoreMLExecutionProvider) == 0) { - return ep_devices[i]; - } - } - - return nullptr; -} - // Returns whether this machine has an NPU that the CoreML EP factory may advertise at the runtime Core ML version. bool CoreMLCanClaimNpu(const HardwareDeviceTypes& hardware) { - return hardware.has_npu && - CoreMLEpFactory::CanClaimDeviceType(OrtHardwareDeviceType_NPU, coreml::util::CoreMLVersion()); + return hardware.has_npu && coreml::util::CoreMLVersion() >= MINIMUM_COREML_VERSION_FOR_NEURAL_ENGINE_SELECTION; } // Returns whether this machine has a GPU that the CoreML EP factory may advertise at the runtime Core ML version. bool CoreMLCanClaimGpu(const HardwareDeviceTypes& hardware) { - return hardware.has_gpu && - CoreMLEpFactory::CanClaimDeviceType(OrtHardwareDeviceType_GPU, coreml::util::CoreMLVersion()); -} - -// GetErrorCode can be called only on a failed Ort::Status. An OK status contains a null OrtStatus*. Verify failure -// first so the helper stops before GetErrorCode if the call unexpectedly succeeds. -// SCOPED_TRACE includes 'selection' in failure messages, making it clear which input failed. -void ExpectInvalidArgument(const Ort::Status& status, std::string_view selection) { - SCOPED_TRACE(selection); - ASSERT_FALSE(status.IsOK()) << "Expected ORT_INVALID_ARGUMENT, but the call succeeded."; - EXPECT_EQ(status.GetErrorCode(), ORT_INVALID_ARGUMENT); + return hardware.has_gpu && coreml::util::CoreMLVersion() >= MINIMUM_COREML_VERSION; } -// Verifies that a session for mul_1.onnx assigns its input to the CoreML EP and produces the expected output. -void AssertMul1SessionRunsOnCoreML(Ort::Session& session) { +// Verifies that a session for mul_1.onnx assigns its input to the CoreML EP. Used as the session checker for +// RunBasicTest, which runs the inference itself afterwards. +void AssertMul1InputAssignedToCoreML(Ort::Session& session) { const OrtApi* c_api = &Ort::GetApi(); const OrtEpDevice* input_ep_device = nullptr; ASSERT_ORTSTATUS_OK(c_api->SessionGetEpDeviceForInputs(session, &input_ep_device, 1)); ASSERT_NE(input_ep_device, nullptr); ASSERT_STREQ(c_api->EpDevice_EpName(input_ep_device), kCoreMLExecutionProvider); +} + +// Verifies the input assignment and runs the inference. Used by the tests that create the session themselves. +void AssertMul1SessionRunsOnCoreML(Ort::Session& session) { + ASSERT_NO_FATAL_FAILURE(AssertMul1InputAssignedToCoreML(session)); std::vector> inputs(1); auto& input = inputs.back(); @@ -473,89 +451,85 @@ TEST(AutoEpSelection, CoreMLEP) { // The V2 path also checks the input assignment directly. CoreMLEPPreferNpu and CoreMLEPPreferGpu perform the // same assignment check for policy-based selection. RunBasicTest(kCoreMLExecutionProvider, std::nullopt, Ort::KeyValuePairs{}, /*select_devices*/ nullptr, - /*test_auto_select*/ true, AssertMul1SessionRunsOnCoreML, /*disable_cpu_ep_fallback*/ true); + /*test_auto_select*/ true, AssertMul1InputAssignedToCoreML, /*disable_cpu_ep_fallback*/ true); } -// Select the CoreML NPU and GPU in a single AppendExecutionProvider_V2 call without specifying MLComputeUnits. -// The factory must derive MLComputeUnits=ALL from the selected devices, and session creation must succeed. -TEST(AutoEpSelection, CoreMLEPMultipleDevices) { - const HardwareDeviceTypes hardware = GetHardwareDeviceTypes(); - if (!CoreMLCanClaimNpu(hardware) || !CoreMLCanClaimGpu(hardware)) { - GTEST_SKIP() << "Need both an NPU and a GPU that the CoreML EP can advertise on this machine."; - } - - const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); - ASSERT_NE(coreml_devices.npu, nullptr) << "CoreML EP did not claim the NPU hardware device."; - ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not claim the GPU hardware device."; - - const auto select_devices = [&](std::vector& devices) { - devices = {coreml_devices.npu, coreml_devices.gpu}; +// Tests explicit device selection through AppendExecutionProvider_V2. Each test case selects one or more CoreML +// devices, optionally provides MLComputeUnits in ep_options, and verifies that the session assigns the graph to +// CoreML and runs successfully. Test cases are skipped individually only when a required device is unavailable. +// Test cases with MLComputeUnits verify that the caller may narrow accelerator use within the selected devices: +// for an NPU-and-GPU selection, CPUAndGPU enables only the GPU accelerator and CPUAndNeuralEngine enables only +// the NPU. CPUOnly disables all selected accelerators while the graph remains assigned to CoreML. +TEST(AutoEpSelection, CoreMLEPExplicitDeviceSelection) { + enum class Selection { kNpuAndGpu, + kGpuOnly, + kNpuOnly, + kEveryAdvertisedDevice }; + + struct TestCase { + const char* description; + Selection selection; + const char* compute_units; // nullptr: MLComputeUnits omitted }; - // MLComputeUnits is intentionally omitted. If ALL were supplied explicitly, the factory would validate it instead - // of deriving it from the selected NPU and GPU. The separate CoreMLEPDefaultComputeUnits test checks this - // derivation directly. This test verifies the same behavior during session creation. - // CoreMLEPNarrowingComputeUnitsOverride covers the same multi-device V2 path with non-empty ep_options. - // select_devices applies only to the AppendExecutionProvider_V2 path. The "test.ep_to_select" path passes only the - // first CoreML device to the factory. That would test only one device, which CoreMLEP already covers, rather than - // a selection containing both the NPU and GPU. Run only the V2 path, verify its input assignment directly, and - // disable ORT CPU fallback so CoreML must execute the entire graph. - RunBasicTest(kCoreMLExecutionProvider, std::nullopt, Ort::KeyValuePairs{}, select_devices, - /*test_auto_select*/ false, AssertMul1SessionRunsOnCoreML, /*disable_cpu_ep_fallback*/ true); -} + const TestCase test_cases[] = { + {"NPU and GPU, MLComputeUnits omitted", Selection::kNpuAndGpu, nullptr}, + {"NPU and GPU, CPUAndGPU", Selection::kNpuAndGpu, "CPUAndGPU"}, + {"NPU and GPU, CPUAndNeuralEngine", Selection::kNpuAndGpu, "CPUAndNeuralEngine"}, + {"GPU only, MLComputeUnits omitted", Selection::kGpuOnly, nullptr}, + {"NPU only, MLComputeUnits omitted", Selection::kNpuOnly, nullptr}, + {"every advertised device, CPUOnly", Selection::kEveryAdvertisedDevice, "CPUOnly"}, + }; -// Verifies that AppendExecutionProvider_V2 passes both selected devices to the factory. CPUAndGPU is accepted only -// when the GPU is present in the factory's device list, and CPUAndNeuralEngine only when the NPU is present. -// Both sessions must be created successfully, so the test fails if either device is missing. -TEST(AutoEpSelection, CoreMLEPMultipleDevicesBothReachTheFactory) { const HardwareDeviceTypes hardware = GetHardwareDeviceTypes(); - if (!CoreMLCanClaimNpu(hardware) || !CoreMLCanClaimGpu(hardware)) { - GTEST_SKIP() << "Need both an NPU and a GPU that the CoreML EP can advertise on this machine."; + const bool can_claim_npu = CoreMLCanClaimNpu(hardware); + const bool can_claim_gpu = CoreMLCanClaimGpu(hardware); + if (!can_claim_npu && !can_claim_gpu) { + GTEST_SKIP() << "No hardware device that the CoreML EP factory can advertise on this machine."; } const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); - ASSERT_NE(coreml_devices.npu, nullptr) << "CoreML EP did not advertise the NPU hardware device."; - ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not advertise the GPU hardware device."; - - std::vector ep_devices; - ep_devices.emplace_back(coreml_devices.npu); - ep_devices.emplace_back(coreml_devices.gpu); + if (can_claim_npu) { + ASSERT_NE(coreml_devices.npu, nullptr) << "CoreML EP did not claim the NPU hardware device."; + } + if (can_claim_gpu) { + ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not claim the GPU hardware device."; + } - for (const char* compute_units : {kCoremlProviderOption_MLComputeUnits_CPUAndGPU, - kCoremlProviderOption_MLComputeUnits_CPUAndNeuralEngine}) { - SCOPED_TRACE(compute_units); + for (const TestCase& test_case : test_cases) { + SCOPED_TRACE(test_case.description); - Ort::KeyValuePairs ep_options; - ep_options.Add(kCoremlProviderOption_MLComputeUnits, compute_units); + const bool wants_npu = test_case.selection == Selection::kNpuAndGpu || + test_case.selection == Selection::kNpuOnly || + (test_case.selection == Selection::kEveryAdvertisedDevice && can_claim_npu); + const bool wants_gpu = test_case.selection == Selection::kNpuAndGpu || + test_case.selection == Selection::kGpuOnly || + (test_case.selection == Selection::kEveryAdvertisedDevice && can_claim_gpu); + if ((wants_npu && !can_claim_npu) || (wants_gpu && !can_claim_gpu)) { + continue; + } - Ort::SessionOptions session_options; - session_options.AppendExecutionProvider_V2(*ort_env, ep_devices, ep_options); + std::vector devices; + if (wants_npu) { + devices.push_back(coreml_devices.npu); + } + if (wants_gpu) { + devices.push_back(coreml_devices.gpu); + } - // AppendExecutionProvider_V2 adds both devices and the current MLComputeUnits value to session_options. - // Session creation must succeed because MLComputeUnits enables an accelerator included in the selection. - Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); - ASSERT_NO_FATAL_FAILURE(AssertMul1SessionRunsOnCoreML(session)); - } -} + Ort::KeyValuePairs ep_options; + if (test_case.compute_units != nullptr) { + ep_options.Add(kCoremlProviderOption_MLComputeUnits, test_case.compute_units); + } -// Select only the CoreML GPU device explicitly via AppendExecutionProvider_V2 (the CPUAndGPU default path). -TEST(AutoEpSelection, CoreMLEPExplicitGpu) { - if (!CoreMLCanClaimGpu(GetHardwareDeviceTypes())) { - GTEST_SKIP() << "No GPU hardware device that the CoreML EP factory can advertise on this machine."; + // Run only the AppendExecutionProvider_V2 path because it passes the test case's full device selection to the + // factory. The "test.ep_to_select" path would pass only the first CoreML device. Verify the input assignment and + // disable ORT CPU fallback so CoreML must execute the entire graph. + RunBasicTest( + kCoreMLExecutionProvider, std::nullopt, ep_options, + [&devices](std::vector& selected) { selected = devices; }, + /*test_auto_select*/ false, AssertMul1InputAssignedToCoreML, /*disable_cpu_ep_fallback*/ true); } - - const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); - ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not claim the GPU hardware device."; - - const auto select_devices = [&](std::vector& devices) { - devices = {coreml_devices.gpu}; - }; - - // select_devices affects only the AppendExecutionProvider_V2 path. The "test.ep_to_select" path selects - // only the first CoreML device, so it cannot guarantee a GPU-only selection. Verify that the session uses - // CoreML, and disable ORT CPU fallback to prevent a false pass. - RunBasicTest(kCoreMLExecutionProvider, std::nullopt, Ort::KeyValuePairs{}, select_devices, - /*test_auto_select*/ false, AssertMul1SessionRunsOnCoreML, /*disable_cpu_ep_fallback*/ true); } // PREFER_NPU must select the CoreML EP for the Apple Neural Engine instead of falling back to the ORT CPU EP. @@ -626,145 +600,103 @@ TEST(AutoEpSelection, CoreMLEPPreferGpu) { } } -// A session created through the legacy AppendExecutionProvider API has no selected OrtEpDevice. -// SessionGetEpDeviceForInputs therefore matches the assigned EP name against the OrtEpDevices returned by -// GetEpDevices and reports the first match. Before CoreML was exposed through OrtEpFactory, GetEpDevices returned -// no CoreML OrtEpDevices, so this lookup found no match and the API reported nullptr. With the factory registered, -// the lookup reports the first advertised CoreML device, which may be an NPU or a GPU. -// Because the lookup matches only the EP name, the reported device does not reflect the session's MLComputeUnits. -// A CPUOnly session therefore reports the first advertised CoreML device. With the current factory, that device is -// an NPU or a GPU. Advertising a CPU device would not resolve this mismatch because the lookup would still return -// the first CoreML device in GetEpDevices, regardless of MLComputeUnits. This test captures the current behavior, -// not a final API contract. It remains undecided which device, if any, a legacy CoreML session should report. -TEST(AutoEpSelection, CoreMLLegacySessionReportsFirstAdvertisedEpDevice) { - const OrtEpDevice* first_coreml_device = GetFirstCoreMLEpDevice(); - if (first_coreml_device == nullptr) { - GTEST_SKIP() << "CoreML EP advertised no OrtEpDevice on this machine, so the lookup has nothing to report."; +// MLComputeUnits must not enable an accelerator outside the selected devices. The policy path uses PREFER_NPU to +// select only the NPU, then tests CPUAndGPU and ALL. The AppendExecutionProvider_V2 path selects only the GPU, then +// tests CPUAndNeuralEngine and ALL. Each value must be rejected because it enables an unselected accelerator. +TEST(AutoEpSelection, CoreMLEPConflictingComputeUnitsRejected) { + const HardwareDeviceTypes hardware = GetHardwareDeviceTypes(); + const bool can_claim_npu = CoreMLCanClaimNpu(hardware); + const bool can_claim_gpu = CoreMLCanClaimGpu(hardware); + if (!can_claim_npu && !can_claim_gpu) { + GTEST_SKIP() << "No hardware device that the CoreML EP factory can advertise on this machine."; } - Ort::SessionOptions session_options; - session_options.AppendExecutionProvider( - kCoreMLExecutionProvider, - {{kCoremlProviderOption_MLComputeUnits, kCoremlProviderOption_MLComputeUnits_CPUOnly}}); - Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); - - const OrtApi* c_api = &Ort::GetApi(); - const OrtEpDevice* input_ep_device = nullptr; - ASSERT_ORTSTATUS_OK(c_api->SessionGetEpDeviceForInputs(session, &input_ep_device, 1)); - - ASSERT_NE(input_ep_device, nullptr) << "Expected the legacy CoreML session to report an OrtEpDevice."; - EXPECT_STREQ(c_api->EpDevice_EpName(input_ep_device), kCoreMLExecutionProvider); - EXPECT_EQ(input_ep_device, first_coreml_device) - << "The legacy session has no selected OrtEpDevice, so the current name-based lookup should report the first " - "advertised CoreML device."; - - // SessionGetEpDeviceForOutputs performs its own device lookup, so check it separately from - // SessionGetEpDeviceForInputs. - const OrtEpDevice* output_ep_device = nullptr; - ASSERT_ORTSTATUS_OK(c_api->SessionGetEpDeviceForOutputs(session, &output_ep_device, 1)); - - ASSERT_NE(output_ep_device, nullptr) - << "Expected SessionGetEpDeviceForOutputs to report an OrtEpDevice for the legacy CoreML session."; - EXPECT_STREQ(c_api->EpDevice_EpName(output_ep_device), kCoreMLExecutionProvider); - EXPECT_EQ(output_ep_device, first_coreml_device) - << "SessionGetEpDeviceForOutputs must report the same first advertised CoreML device as " - "SessionGetEpDeviceForInputs."; -} - -// Verifies how the factory sets MLComputeUnits when the caller does not specify it: CPUAndNeuralEngine for NPU, -// CPUAndGPU for GPU, and ALL for NPU plus GPU. Also verifies that a compatible user-provided value is preserved. -// The test uses synthetic hardware devices, so it also runs on Apple systems where discovery reports no NPU or -// GPU, such as Intel Macs. -TEST(AutoEpSelection, CoreMLEPDefaultComputeUnits) { - OrtHardwareDevice npu{}; - npu.type = OrtHardwareDeviceType_NPU; - OrtHardwareDevice gpu{}; - gpu.type = OrtHardwareDeviceType_GPU; - - const auto default_compute_units = [](std::initializer_list devices) -> std::string { - ProviderOptions options; - Ort::Status status{CoreMLEpFactory::ValidateDeviceSelectionAndResolveMLComputeUnits( - devices.begin(), devices.size(), options)}; - if (!status.IsOK()) { - ADD_FAILURE() << status.GetErrorMessage(); - return {}; - } + const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); - const auto it = options.find(kCoremlProviderOption_MLComputeUnits); - if (it == options.end()) { - ADD_FAILURE() << "MLComputeUnits was not set."; - return {}; + const auto expect_rejected = [](Ort::SessionOptions& session_options, const char* explanation, + bool check_error_code) { + try { + Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); + FAIL() << "Expected session creation to fail: " << explanation; + } catch (const Ort::Exception& ex) { + if (check_error_code) { + EXPECT_EQ(ex.GetOrtErrorCode(), ORT_INVALID_ARGUMENT); + } + EXPECT_THAT(ex.what(), ::testing::HasSubstr("was not selected")); } - - return it->second; }; - EXPECT_EQ(default_compute_units({&npu}), "CPUAndNeuralEngine"); - EXPECT_EQ(default_compute_units({&gpu}), "CPUAndGPU"); - EXPECT_EQ(default_compute_units({&npu, &gpu}), "ALL"); - - // A compatible user-provided MLComputeUnits value is preserved. - ProviderOptions user_options{{kCoremlProviderOption_MLComputeUnits, "CPUOnly"}}; - const OrtHardwareDevice* npu_only[] = {&npu}; - Ort::Status status{ - CoreMLEpFactory::ValidateDeviceSelectionAndResolveMLComputeUnits(npu_only, 1, user_options)}; - ASSERT_TRUE(status.IsOK()) << status.GetErrorMessage(); - EXPECT_EQ(user_options[kCoremlProviderOption_MLComputeUnits], "CPUOnly"); -} + if (can_claim_npu) { + ASSERT_NE(coreml_devices.npu, nullptr) << "CoreML EP did not claim the NPU hardware device."; -// Verifies that MLComputeUnits reaches the factory through SessionOptions. With PREFER_NPU, -// MLComputeUnits=CPUAndGPU must be rejected because the policy did not select the GPU. -TEST(AutoEpSelection, CoreMLEPPreferNpuConflictingComputeUnits) { - if (!CoreMLCanClaimNpu(GetHardwareDeviceTypes())) { - GTEST_SKIP() << "No NPU hardware device, or this Core ML version cannot advertise it."; + for (const char* compute_units : {"CPUAndGPU", "ALL"}) { + SCOPED_TRACE(compute_units); + Ort::SessionOptions session_options; + session_options.SetEpSelectionPolicy(OrtExecutionProviderDevicePolicy_PREFER_NPU); + const std::string option_key = + OrtSessionOptions::GetProviderOptionPrefix(kCoreMLExecutionProvider) + kCoremlProviderOption_MLComputeUnits; + session_options.AddConfigEntry(option_key.c_str(), compute_units); + expect_rejected(session_options, "the value enables the GPU, which PREFER_NPU did not select.", + /*check_error_code*/ true); + } } - ASSERT_NE(GetCoreMLEpDevices().npu, nullptr) << "CoreML EP did not claim the NPU hardware device."; - - Ort::SessionOptions session_options; - session_options.SetEpSelectionPolicy(OrtExecutionProviderDevicePolicy_PREFER_NPU); - const std::string option_key = - OrtSessionOptions::GetProviderOptionPrefix(kCoreMLExecutionProvider) + kCoremlProviderOption_MLComputeUnits; - session_options.AddConfigEntry(option_key.c_str(), "CPUAndGPU"); + if (can_claim_gpu) { + ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not claim the GPU hardware device."; - try { - Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); - FAIL() << "Session creation should have failed: MLComputeUnits=CPUAndGPU conflicts with the NPU selection."; - } catch (const Ort::Exception& ex) { - EXPECT_EQ(ex.GetOrtErrorCode(), ORT_INVALID_ARGUMENT); - EXPECT_THAT(ex.what(), ::testing::HasSubstr("was not selected")); + for (const char* compute_units : {"CPUAndNeuralEngine", "ALL"}) { + SCOPED_TRACE(compute_units); + Ort::KeyValuePairs ep_options; + ep_options.Add(kCoremlProviderOption_MLComputeUnits, compute_units); + std::vector ep_devices{Ort::ConstEpDevice{coreml_devices.gpu}}; + Ort::SessionOptions session_options; + session_options.AppendExecutionProvider_V2(*ort_env, ep_devices, ep_options); + // The V2 registration path currently reports the factory's ORT_INVALID_ARGUMENT as ORT_FAIL, so only the + // diagnostic message is verified here. The policy path above verifies ORT_INVALID_ARGUMENT. + expect_rejected(session_options, "the value enables the NPU, but only the GPU was selected.", + /*check_error_code*/ false); + } } } -// Verifies that AppendExecutionProvider_V2 passes ep_options to factory validation. With an explicit GPU-only -// selection, MLComputeUnits=CPUAndNeuralEngine must be rejected because the NPU was not selected. -TEST(AutoEpSelection, CoreMLEPExplicitGpuConflictingComputeUnits) { - if (!CoreMLCanClaimGpu(GetHardwareDeviceTypes())) { - GTEST_SKIP() << "No GPU hardware device that the CoreML EP factory can advertise on this machine."; +// The factory must reject selecting the same device type twice. AppendExecutionProvider_V2 validation +// checks only that all selected devices use the same EP and factory, so the duplicate reaches the factory-specific +// validation. +TEST(AutoEpSelection, CoreMLEPDuplicateDeviceSelectionRejected) { + const HardwareDeviceTypes hardware = GetHardwareDeviceTypes(); + if (!CoreMLCanClaimNpu(hardware) && !CoreMLCanClaimGpu(hardware)) { + GTEST_SKIP() << "No hardware device that the CoreML EP factory can advertise on this machine."; } const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); - ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not claim the GPU hardware device."; + ASSERT_TRUE(coreml_devices.npu != nullptr || coreml_devices.gpu != nullptr) + << "CoreML EP did not advertise any supported NPU or GPU device."; - Ort::SessionOptions session_options; - Ort::KeyValuePairs ep_options; - ep_options.Add(kCoremlProviderOption_MLComputeUnits, "CPUAndNeuralEngine"); - std::vector ep_devices{Ort::ConstEpDevice{coreml_devices.gpu}}; - session_options.AppendExecutionProvider_V2(*ort_env, ep_devices, ep_options); + // The factory checks for duplicate NPU and GPU selections separately, so test each available device type. + for (const OrtEpDevice* device : {coreml_devices.npu, coreml_devices.gpu}) { + if (device == nullptr) { + continue; + } + SCOPED_TRACE(testing::Message() << "device type " + << Ort::GetApi().HardwareDevice_Type(Ort::GetApi().EpDevice_Device(device))); - try { - Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); - FAIL() << "Session creation should have failed: MLComputeUnits=CPUAndNeuralEngine conflicts with the GPU " - "selection."; - } catch (const Ort::Exception& ex) { - // Verify the diagnostic message independently of the V2 error-code handling. - // CoreMLEPPreferNpuConflictingComputeUnits verifies ORT_INVALID_ARGUMENT through the policy path. - EXPECT_THAT(ex.what(), ::testing::HasSubstr("was not selected")); + std::vector ep_devices{Ort::ConstEpDevice{device}, Ort::ConstEpDevice{device}}; + Ort::SessionOptions session_options; + session_options.AppendExecutionProvider_V2(*ort_env, ep_devices, Ort::KeyValuePairs{}); + + try { + Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); + FAIL() << "Expected session creation to fail: the same CoreML device was selected twice."; + } catch (const Ort::Exception& ex) { + // The V2 registration path currently reports the factory's ORT_INVALID_ARGUMENT as ORT_FAIL, so this test + // verifies only the diagnostic message. + EXPECT_THAT(ex.what(), ::testing::HasSubstr("At most one device of each type can be selected")); + } } } -// Verifies that an unknown MLComputeUnits value reaches CoreMLOptions and is rejected during provider creation. -// CoreMLEPComputeUnitsOverrideValidation separately verifies that the factory passes the value through. +// Verifies that an unknown MLComputeUnits value is not rejected by the factory but reaches CoreMLOptions, which +// rejects it during provider creation, so session creation fails with the CoreMLOptions diagnostic. TEST(AutoEpSelection, CoreMLEPUnknownComputeUnitsRejectedByProvider) { const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); const OrtEpDevice* device = coreml_devices.npu != nullptr ? coreml_devices.npu : coreml_devices.gpu; @@ -781,320 +713,146 @@ TEST(AutoEpSelection, CoreMLEPUnknownComputeUnitsRejectedByProvider) { try { Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); - FAIL() << "Session creation should have failed: MLComputeUnits=NotAComputeUnitsValue is not a valid value."; + FAIL() << "Expected session creation to fail: MLComputeUnits=NotAComputeUnitsValue is not a valid value."; } catch (const Ort::Exception& ex) { EXPECT_THAT(ex.what(), ::testing::HasSubstr("Invalid value for option")); EXPECT_THAT(ex.what(), ::testing::HasSubstr("NotAComputeUnitsValue")); } } -// Verifies that MLComputeUnits=CPUOnly can narrow an accelerator selection. The session must be created and run -// successfully, and the graph must remain assigned to the CoreML EP. CoreML may enable only accelerators represented -// by the selected OrtEpDevices. This does not prevent Core ML from using its internal CPU path. -// MLComputeUnits=CPUOnly also does not reassign the graph to the ORT CPU EP. -TEST(AutoEpSelection, CoreMLEPNarrowingComputeUnitsOverride) { - const HardwareDeviceTypes hardware = GetHardwareDeviceTypes(); - const bool can_claim_npu = CoreMLCanClaimNpu(hardware); - const bool can_claim_gpu = CoreMLCanClaimGpu(hardware); - if (!can_claim_npu && !can_claim_gpu) { - GTEST_SKIP() << "No hardware device that the CoreML EP factory can advertise on this machine."; - } - - const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); - std::vector ep_devices; - if (can_claim_npu) { - ASSERT_NE(coreml_devices.npu, nullptr) << "CoreML EP did not claim the NPU hardware device."; - ep_devices.emplace_back(coreml_devices.npu); - } - if (can_claim_gpu) { - ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not claim the GPU hardware device."; - ep_devices.emplace_back(coreml_devices.gpu); - } - - Ort::KeyValuePairs ep_options; - ep_options.Add(kCoremlProviderOption_MLComputeUnits, "CPUOnly"); - - Ort::SessionOptions session_options; - session_options.AppendExecutionProvider_V2(*ort_env, ep_devices, ep_options); - Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); +// The two tests below call CoreMLEpFactory through its EpFactoryInternal wrapper using synthetic hardware devices. +// Device advertising depends only on the device type and runtime Core ML version, so the tests require no physical +// accelerator and can run on Intel Macs that meet the version requirement. Any published OrtEpDevice instances are +// released through the EP API. +namespace { - ASSERT_NO_FATAL_FAILURE(AssertMul1SessionRunsOnCoreML(session)); +// Creates a synthetic hardware device of the given type. The device's vendor does not affect whether the factory +// advertises the device. The Apple values below match those reported by device discovery. +OrtHardwareDevice MakeSyntheticHardwareDevice(OrtHardwareDeviceType type) { + OrtHardwareDevice device{}; + device.type = type; + device.vendor_id = 0x106B; // Apple's PCI vendor ID + device.vendor = "Apple"; + return device; } -// Verifies that a user-provided MLComputeUnits value is compatible with the selected accelerators. The value -// may disable selected accelerators, as CPUOnly does, but it must not enable an accelerator that was not selected. -TEST(AutoEpSelection, CoreMLEPComputeUnitsOverrideValidation) { - OrtHardwareDevice npu{}; - npu.type = OrtHardwareDeviceType_NPU; - OrtHardwareDevice gpu{}; - gpu.type = OrtHardwareDeviceType_GPU; - - const auto validate = [](std::initializer_list devices, - std::string_view compute_units) { - ProviderOptions options{{kCoremlProviderOption_MLComputeUnits, std::string{compute_units}}}; - return Ort::Status{CoreMLEpFactory::ValidateDeviceSelectionAndResolveMLComputeUnits( - devices.begin(), devices.size(), options)}; - }; - - // Values that enable only selected accelerators, or disable them with CPUOnly, are allowed. - EXPECT_TRUE(validate({&npu}, "CPUAndNeuralEngine").IsOK()); - EXPECT_TRUE(validate({&npu}, "CPUOnly").IsOK()); - EXPECT_TRUE(validate({&gpu}, "CPUAndGPU").IsOK()); - EXPECT_TRUE(validate({&gpu}, "CPUOnly").IsOK()); - EXPECT_TRUE(validate({&npu, &gpu}, "ALL").IsOK()); - EXPECT_TRUE(validate({&npu, &gpu}, "CPUAndNeuralEngine").IsOK()); - EXPECT_TRUE(validate({&npu, &gpu}, "CPUAndGPU").IsOK()); - EXPECT_TRUE(validate({&npu, &gpu}, "CPUOnly").IsOK()); - - // Values that enable an unselected accelerator are rejected. - ExpectInvalidArgument(validate({&npu}, "CPUAndGPU"), "NPU selected, CPUAndGPU requested"); - ExpectInvalidArgument(validate({&npu}, "ALL"), "NPU selected, ALL requested"); - ExpectInvalidArgument(validate({&gpu}, "CPUAndNeuralEngine"), "GPU selected, CPUAndNeuralEngine requested"); - ExpectInvalidArgument(validate({&gpu}, "ALL"), "GPU selected, ALL requested"); - - // This factory helper checks only whether MLComputeUnits is compatible with the selected devices. - // Unknown values are left for CoreMLOptions to reject during provider creation. - EXPECT_TRUE(validate({&npu}, "NotAComputeUnitsValue").IsOK()); -} +// Number of calls to the injected GetVersion callback. +int g_injected_version_calls = 0; -// Tests which device selections the factory accepts or rejects. -// Valid selections contain one NPU, one GPU, or one NPU plus one GPU. -// Empty lists, CPU devices, duplicate device types, a null device entry, and a null device-list pointer with a -// nonzero device count must return ORT_INVALID_ARGUMENT. -TEST(AutoEpSelection, CoreMLEPDeviceValidation) { - OrtHardwareDevice npu{}; - npu.type = OrtHardwareDeviceType_NPU; - OrtHardwareDevice gpu{}; - gpu.type = OrtHardwareDeviceType_GPU; - OrtHardwareDevice cpu{}; - cpu.type = OrtHardwareDeviceType_CPU; - - const auto validate = [](std::initializer_list devices) { - ProviderOptions options; - return Ort::Status{CoreMLEpFactory::ValidateDeviceSelectionAndResolveMLComputeUnits( - devices.begin(), devices.size(), options)}; - }; - - // The valid selections, for contrast. - EXPECT_TRUE(validate({&npu}).IsOK()); - EXPECT_TRUE(validate({&gpu}).IsOK()); - EXPECT_TRUE(validate({&npu, &gpu}).IsOK()); - - ExpectInvalidArgument(validate({}), "no devices"); - ExpectInvalidArgument(validate({&cpu}), "CPU only: the factory never claims the CPU device"); - ExpectInvalidArgument(validate({&cpu, &gpu}), "GPU mixed with an unsupported device"); - ExpectInvalidArgument(validate({&npu, &npu}), "duplicate NPU"); - ExpectInvalidArgument(validate({&gpu, &gpu}), "duplicate GPU"); - - ExpectInvalidArgument(validate({nullptr}), "null device"); - ProviderOptions options; - const Ort::Status null_device_list_status{ - CoreMLEpFactory::ValidateDeviceSelectionAndResolveMLComputeUnits(nullptr, 1, options)}; - ExpectInvalidArgument(null_device_list_status, "null device list"); -} +} // namespace -// Verifies CanClaimDeviceType independently of the host hardware and runtime Core ML version. -// This test covers the Core ML 5 minimum, the Core ML 6 NPU requirement, and the version-independent CPU exclusion. -TEST(AutoEpSelection, CoreMLEPClaimabilityByVersion) { - const auto can_claim = [](OrtHardwareDeviceType type, int32_t coreml_version) { - return CoreMLEpFactory::CanClaimDeviceType(type, coreml_version); - }; +// Verifies that GetSupportedDevices preserves input order, advertises at most one NPU and one GPU, ignores +// duplicate devices of either type, and excludes CPU devices. +// It publishes no more than max_ep_devices devices and leaves the remaining output entries unchanged. +// Every successful call writes the output count, including zero when no devices are advertised. +TEST(AutoEpSelection, CoreMLEPGetSupportedDevicesEnumeration) { + if (coreml::util::CoreMLVersion() < MINIMUM_COREML_VERSION_FOR_NEURAL_ENGINE_SELECTION) { + GTEST_SKIP() << "This test requires Core ML 6 or later because it expects the factory to advertise an NPU."; + } - // Below the EP's Core ML 5 minimum (provider creation would fail) nothing is advertised. - EXPECT_FALSE(can_claim(OrtHardwareDeviceType_NPU, 4)); - EXPECT_FALSE(can_claim(OrtHardwareDeviceType_GPU, 4)); + EpFactoryInternal factory{std::make_unique()}; + OrtEpFactory* c_factory = &factory; + ASSERT_NE(c_factory->GetSupportedDevices, nullptr); + + const OrtHardwareDevice gpu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_GPU); + const OrtHardwareDevice npu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_NPU); + const OrtHardwareDevice second_gpu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_GPU); + const OrtHardwareDevice second_npu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_NPU); + const OrtHardwareDevice cpu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_CPU); + + constexpr size_t kOutputCapacity = 5; + OrtEpDevice untouched_marker{}; + + // Calls GetSupportedDevices on the given hardware devices with max_ep_devices as the output capacity. Returns the + // hardware device behind each published OrtEpDevice in publication order and releases the instances. Output + // entries beyond the published count must keep their marker value. + const auto published_hardware = [&](gsl::span hardware, size_t max_ep_devices) { + std::array ep_devices{}; + ep_devices.fill(&untouched_marker); + size_t num_ep_devices = 42; // a successful call must overwrite this + Ort::Status status{c_factory->GetSupportedDevices(c_factory, hardware.data(), hardware.size(), + ep_devices.data(), max_ep_devices, &num_ep_devices)}; + + InlinedVector result; + EXPECT_TRUE(status.IsOK()) << status.GetErrorMessage(); + EXPECT_LE(num_ep_devices, max_ep_devices); + if (!status.IsOK() || num_ep_devices > max_ep_devices) { + return result; + } - // Core ML 5: the GPU is advertised, the NPU is not (CPUAndNeuralEngine requires Core ML 6). - EXPECT_FALSE(can_claim(OrtHardwareDeviceType_NPU, 5)); - EXPECT_TRUE(can_claim(OrtHardwareDeviceType_GPU, 5)); + for (size_t i = 0; i < ep_devices.size(); ++i) { + if (i >= num_ep_devices) { + EXPECT_EQ(ep_devices[i], &untouched_marker) << "Entry " << i << " lies beyond the published count."; + continue; + } - // Core ML 6+: both. - EXPECT_TRUE(can_claim(OrtHardwareDeviceType_NPU, 6)); - EXPECT_TRUE(can_claim(OrtHardwareDeviceType_GPU, 6)); - EXPECT_TRUE(can_claim(OrtHardwareDeviceType_NPU, 7)); - EXPECT_TRUE(can_claim(OrtHardwareDeviceType_GPU, 7)); + EXPECT_NE(ep_devices[i], &untouched_marker) << "Published entry " << i << " was not written."; + EXPECT_NE(ep_devices[i], nullptr) << "Published entry " << i << " is null."; + if (ep_devices[i] == nullptr || ep_devices[i] == &untouched_marker) { + continue; + } - // The CPU device is never claimed regardless of version (left to the CPU EP). - EXPECT_FALSE(can_claim(OrtHardwareDeviceType_CPU, 4)); - EXPECT_FALSE(can_claim(OrtHardwareDeviceType_CPU, 8)); -} + result.push_back(Ort::GetApi().EpDevice_Device(ep_devices[i])); + Ort::GetEpApi().ReleaseEpDevice(ep_devices[i]); + } -// Tests SelectDevicesToClaim with synthetic hardware devices instead of relying on host discovery. Covers CPU -// filtering, input order, duplicate devices of the same type, the max_ep_devices limit, and version checks -// independently of the Core ML version installed on the host. -TEST(AutoEpSelection, CoreMLEPSelectDevicesToClaim) { - OrtHardwareDevice npu0{}; - npu0.type = OrtHardwareDeviceType_NPU; - OrtHardwareDevice npu1{}; - npu1.type = OrtHardwareDeviceType_NPU; - OrtHardwareDevice gpu0{}; - gpu0.type = OrtHardwareDeviceType_GPU; - OrtHardwareDevice gpu1{}; - gpu1.type = OrtHardwareDeviceType_GPU; - OrtHardwareDevice cpu{}; - cpu.type = OrtHardwareDeviceType_CPU; - - constexpr int32_t coreml6 = 6; - const auto select = [](std::initializer_list devices, int32_t coreml_version, - size_t max_ep_devices = 8) { - return CoreMLEpFactory::SelectDevicesToClaim(gsl::make_span(devices.begin(), devices.size()), coreml_version, - max_ep_devices); + return result; }; - // CPU + NPU + GPU -> the CPU is filtered out. The NPU and GPU remain in input order. - { - const auto selected = select({&cpu, &npu0, &gpu0}, coreml6); - ASSERT_EQ(selected.size(), 2u); - EXPECT_EQ(selected[0], &npu0); - EXPECT_EQ(selected[1], &gpu0); - } + // Only the first GPU and NPU are advertised, in that order. + const std::array gpu_first{&gpu, &npu, &second_gpu, &second_npu, &cpu}; + EXPECT_THAT(published_hardware(gpu_first, kOutputCapacity), ::testing::ElementsAre(&gpu, &npu)); - // Repeat the NPU+GPU selection with the GPU first. Together with the NPU-first case above, this verifies that - // selection preserves input order instead of sorting by device type. - { - const auto selected = select({&cpu, &gpu0, &npu0}, coreml6); - ASSERT_EQ(selected.size(), 2u); - EXPECT_EQ(selected[0], &gpu0); - EXPECT_EQ(selected[1], &npu0); - } - - // Duplicate devices of a type: only the first of each type is advertised. - { - const auto selected = select({&gpu0, &gpu1}, coreml6); - ASSERT_EQ(selected.size(), 1u); - EXPECT_EQ(selected[0], &gpu0); - } - { - const auto selected = select({&npu0, &npu1, &gpu0, &gpu1}, coreml6); - ASSERT_EQ(selected.size(), 2u); - EXPECT_EQ(selected[0], &npu0); - EXPECT_EQ(selected[1], &gpu0); - } + // Skip the leading CPU device and preserve the NPU-before-GPU order. + const std::array cpu_first{&cpu, &npu, &gpu}; + EXPECT_THAT(published_hardware(cpu_first, kOutputCapacity), ::testing::ElementsAre(&npu, &gpu)); - // max_ep_devices limits the number of selected devices. - { - const auto selected = select({&npu0, &gpu0}, coreml6, /*max_ep_devices*/ 1); - ASSERT_EQ(selected.size(), 1u); - EXPECT_EQ(selected[0], &npu0); - } + // The output capacity limits the total number of published devices. A capacity of 1 keeps only the first + // supported device. A capacity of 0 produces no devices. + EXPECT_THAT(published_hardware(gpu_first, 1), ::testing::ElementsAre(&gpu)); + EXPECT_THAT(published_hardware(gpu_first, 0), ::testing::IsEmpty()); - // Verify that selection applies the CanClaimDeviceType version checks. - { - const auto selected = select({&npu0, &gpu0}, /*coreml_version*/ 5); - ASSERT_EQ(selected.size(), 1u); - EXPECT_EQ(selected[0], &gpu0); - } - EXPECT_TRUE(select({&npu0, &gpu0}, /*coreml_version*/ 4).empty()); + // When the input contains only a CPU device, no devices are published and the output count is set to zero. + const std::array cpu_only{&cpu}; + EXPECT_THAT(published_hardware(cpu_only, kOutputCapacity), ::testing::IsEmpty()); } -// Tests CreateAndPublishEpDevices with injected create and release callbacks. On failure, all previously -// created devices must be released and the output parameters must remain unchanged. On success, all device -// pointers must be published without releasing any devices. The dummy OrtEpDevice objects are never dereferenced. -// Their addresses identify which devices are published or released. -TEST(AutoEpSelection, CoreMLEPCreateAndPublishEpDevices) { - OrtHardwareDevice npu{}; - npu.type = OrtHardwareDeviceType_NPU; - OrtHardwareDevice gpu{}; - gpu.type = OrtHardwareDeviceType_GPU; - const std::array selected{&npu, &gpu}; - - OrtEpDevice dummy_device0{}; - OrtEpDevice dummy_device1{}; - - InlinedVector released; - const auto release = [&released](OrtEpDevice* ep_device) { released.push_back(ep_device); }; - - // Initialize the caller-owned output array with distinct non-null pointers. Failure cases verify that these values - // remain unchanged. Initializing the array with nullptr would not distinguish "left untouched" from - // "cleared to nullptr". - OrtEpDevice sentinel0{}; - OrtEpDevice sentinel1{}; - const std::array untouched{&sentinel0, &sentinel1}; - - // The second device creation fails. The first device must be released, and the output parameters must remain - // unchanged. - { - size_t create_calls = 0; - const auto create_second_fails = [&](const OrtHardwareDevice&, OrtEpDevice** ep_device) -> OrtStatus* { - if (create_calls++ == 0) { - *ep_device = &dummy_device0; - return nullptr; - } - - return Ort::GetApi().CreateStatus(ORT_FAIL, "injected creation failure"); - }; - - std::array ep_devices = untouched; - size_t num_ep_devices = 42; // Initial value that must remain unchanged on failure. - - Ort::Status status{CoreMLEpFactory::CreateAndPublishEpDevices(gsl::make_span(selected), create_second_fails, - release, ep_devices.data(), &num_ep_devices)}; - ASSERT_FALSE(status.IsOK()); - EXPECT_THAT(status.GetErrorMessage(), ::testing::HasSubstr("injected creation failure")); - EXPECT_THAT(released, ::testing::ElementsAre(&dummy_device0)); - EXPECT_THAT(ep_devices, ::testing::ElementsAreArray(untouched)); - EXPECT_EQ(num_ep_devices, size_t{42}); - } - - // The first device creation fails. No devices need to be released, and the output parameters must remain unchanged. - { - released.clear(); - const auto create_first_fails = [](const OrtHardwareDevice&, OrtEpDevice**) -> OrtStatus* { - return Ort::GetApi().CreateStatus(ORT_FAIL, "injected creation failure"); - }; - - std::array ep_devices = untouched; - size_t num_ep_devices = 42; // Initial value that must remain unchanged on failure. - - Ort::Status status{CoreMLEpFactory::CreateAndPublishEpDevices(gsl::make_span(selected), create_first_fails, - release, ep_devices.data(), &num_ep_devices)}; - ASSERT_FALSE(status.IsOK()); - EXPECT_TRUE(released.empty()); - EXPECT_THAT(ep_devices, ::testing::ElementsAreArray(untouched)); - EXPECT_EQ(num_ep_devices, size_t{42}); +// If the second OrtEpDevice creation fails, GetSupportedDevices must return an error, release the first device, +// and leave the output array and count unchanged, following ORT's C API convention for failed calls. +// The test replaces the GetVersion callback on EpFactoryInternal with one that returns a valid version on the +// first call and an invalid version on the second. CreateEpDevice validates each version, causing the second +// creation to fail. +TEST(AutoEpSelection, CoreMLEPGetSupportedDevicesRollsBackOnCreateFailure) { + if (coreml::util::CoreMLVersion() < MINIMUM_COREML_VERSION_FOR_NEURAL_ENGINE_SELECTION) { + GTEST_SKIP() << "This test requires Core ML 6 or later so the factory can advertise both an NPU and a GPU."; } - // Success: both devices are published, nothing is released. - { - released.clear(); - size_t create_calls = 0; - const auto create_ok = [&](const OrtHardwareDevice&, OrtEpDevice** ep_device) -> OrtStatus* { - *ep_device = (create_calls++ == 0) ? &dummy_device0 : &dummy_device1; - return nullptr; - }; - - std::array ep_devices{nullptr, nullptr}; - size_t num_ep_devices = 0; - - Ort::Status status{CoreMLEpFactory::CreateAndPublishEpDevices(gsl::make_span(selected), create_ok, release, - ep_devices.data(), &num_ep_devices)}; - ASSERT_TRUE(status.IsOK()) << status.GetErrorMessage(); - EXPECT_EQ(num_ep_devices, size_t{2}); - EXPECT_EQ(ep_devices[0], &dummy_device0); - EXPECT_EQ(ep_devices[1], &dummy_device1); - EXPECT_TRUE(released.empty()); - } + EpFactoryInternal factory{std::make_unique()}; + OrtEpFactory* c_factory = &factory; + g_injected_version_calls = 0; + c_factory->GetVersion = [](const OrtEpFactory*) noexcept -> const char* { + // A valid version for the first device and an invalid one for the second, so the second CreateEpDevice fails. + return ++g_injected_version_calls == 1 ? "1.0.0" : "not a version"; + }; - // No devices selected: a successful call still sets the output count to zero, as required by GetSupportedDevices. - { - released.clear(); - const auto create_never_called = [](const OrtHardwareDevice&, OrtEpDevice**) -> OrtStatus* { - ADD_FAILURE() << "create_ep_device must not be called when nothing was selected."; - return Ort::GetApi().CreateStatus(ORT_FAIL, "unexpected call"); - }; - - std::array ep_devices = untouched; - size_t num_ep_devices = 42; - - Ort::Status status{CoreMLEpFactory::CreateAndPublishEpDevices(gsl::span{}, - create_never_called, release, ep_devices.data(), - &num_ep_devices)}; - ASSERT_TRUE(status.IsOK()) << status.GetErrorMessage(); - EXPECT_EQ(num_ep_devices, size_t{0}); - EXPECT_THAT(ep_devices, ::testing::ElementsAreArray(untouched)); - EXPECT_TRUE(released.empty()); - } + const OrtHardwareDevice npu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_NPU); + const OrtHardwareDevice gpu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_GPU); + const std::array hardware{&npu, &gpu}; + + OrtEpDevice untouched_marker{}; + std::array ep_devices{&untouched_marker, &untouched_marker}; + size_t num_ep_devices = 42; + Ort::Status status{c_factory->GetSupportedDevices(c_factory, hardware.data(), hardware.size(), ep_devices.data(), + ep_devices.size(), &num_ep_devices)}; + + ASSERT_FALSE(status.IsOK()) + << "GetSupportedDevices succeeded although the second CreateEpDevice was made to fail."; + EXPECT_EQ(g_injected_version_calls, 2) + << "GetVersion must be called for both device creation attempts."; + EXPECT_THAT(ep_devices, ::testing::Each(&untouched_marker)); + EXPECT_EQ(num_ep_devices, size_t{42}); } + #endif // defined(USE_COREML) && defined(__APPLE__) #if defined(USE_COREML) && !defined(__APPLE__) @@ -1112,7 +870,7 @@ TEST(AutoEpSelection, CoreMLEPIsNotRegisteredOnNonApplePlatforms) { ASSERT_ORTSTATUS_OK(c_api->GetNumHardwareDevices(*ort_env, &num_devices)); ASSERT_GT(num_devices, 0u) << "Expected device discovery to report at least the CPU device."; - std::vector devices(num_devices); + InlinedVector devices(num_devices); ASSERT_ORTSTATUS_OK(c_api->GetHardwareDevices(*ort_env, devices.data(), num_devices)); OrtDeviceEpIncompatibilityDetails* details = nullptr; From e0b1088608b5c2bcfe709ec4c691c3460816141d Mon Sep 17 00:00:00 2001 From: Maxim Davgalev Date: Mon, 7 Sep 2026 22:55:10 -0700 Subject: [PATCH 08/12] Move the CoreML EP selection tests to test_selection_coreml.cc Move the CoreML EP selection tests from test_selection.cc to a dedicated CoreML test file. --- onnxruntime/test/autoep/test_autoep_utils.h | 20 + onnxruntime/test/autoep/test_selection.cc | 646 +---------------- .../test/autoep/test_selection_coreml.cc | 660 ++++++++++++++++++ 3 files changed, 685 insertions(+), 641 deletions(-) create mode 100644 onnxruntime/test/autoep/test_selection_coreml.cc diff --git a/onnxruntime/test/autoep/test_autoep_utils.h b/onnxruntime/test/autoep/test_autoep_utils.h index e8bb42e108cf5..e7f9474e7e5d0 100644 --- a/onnxruntime/test/autoep/test_autoep_utils.h +++ b/onnxruntime/test/autoep/test_autoep_utils.h @@ -4,7 +4,10 @@ #pragma once #include +#include +#include #include +#include #include "core/session/onnxruntime_cxx_api.h" @@ -48,5 +51,22 @@ struct Utils { LoadExampleEpHooksPtr& example_ep_hooks); }; +// Runs the mul_1.onnx inference test with ep_name selected in two ways: +// - automatically through "test.ep_to_select", when test_auto_select is true. +// - explicitly through SessionOptionsAppendExecutionProvider_V2. +// Both paths pass provider_options to the EP: as prefixed session options on the auto-selection path and as +// ep_options on the V2 path. The V2 path uses the devices chosen by select_devices, or the first OrtEpDevice +// advertised for ep_name when no selector is provided. If library_path is provided, the helper registers the +// plugin EP library before creating the session. +// v2_session_checker runs after the V2 session is created and before inference. +// disable_cpu_ep_fallback applies to both paths. It requires the selected EP to handle the entire graph, preventing +// a false pass caused by unsupported nodes falling back to the ORT CPU EP. +void RunBasicTest(const std::string& ep_name, std::optional library_path, + const Ort::KeyValuePairs& provider_options = Ort::KeyValuePairs{}, + const std::function&)>& select_devices = nullptr, + bool test_auto_select = true, + const std::function& v2_session_checker = nullptr, + bool disable_cpu_ep_fallback = false); + } // namespace test } // namespace onnxruntime diff --git a/onnxruntime/test/autoep/test_selection.cc b/onnxruntime/test/autoep/test_selection.cc index f3096ac76e624..f9f88eae28b3d 100644 --- a/onnxruntime/test/autoep/test_selection.cc +++ b/onnxruntime/test/autoep/test_selection.cc @@ -1,32 +1,18 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -#include #include // #include #include -#include #include #include "core/graph/constants.h" -#include "core/session/abi_devices.h" #include "core/session/abi_key_value_pairs.h" #include "core/session/abi_session_options_impl.h" #include "core/session/onnxruntime_cxx_api.h" #include "core/session/onnxruntime_ep_device_ep_metadata_keys.h" #include "core/session/onnxruntime_session_options_config_keys.h" -#if defined(USE_COREML) -#include "core/common/inlined_containers.h" -#endif - -#if defined(USE_COREML) && defined(__APPLE__) -#include "core/providers/coreml/coreml_provider_factory.h" -#include "core/providers/coreml/model/host_utils.h" -#include "core/session/plugin_ep/ep_factory_coreml.h" -#include "core/session/plugin_ep/ep_factory_internal.h" -#endif - #include "test_allocator.h" #include "test/autoep/test_autoep_utils.h" #include "test/shared_lib/utils.h" @@ -170,19 +156,12 @@ static void TestInference(Ort::Env& env, const std::basic_string& mod } } -namespace { void RunBasicTest(const std::string& ep_name, std::optional library_path, - const Ort::KeyValuePairs& provider_options = Ort::KeyValuePairs{}, - const std::function&)>& select_devices = nullptr, - bool test_auto_select = true, - // Optional callback for the session created through AppendExecutionProvider_V2. - const std::function& v2_session_checker = nullptr, - // Applied to both paths. In the AppendExecutionProvider_V2 path, v2_session_checker verifies the - // input assignment directly, while disabling CPU fallback requires the selected EP to handle the - // entire graph. The "test.ep_to_select" path has no session checker, so disabling CPU fallback - // prevents a false pass if any part of the graph is assigned to the ORT CPU EP instead of the - // selected EP. - bool disable_cpu_ep_fallback = false) { + const Ort::KeyValuePairs& provider_options, + const std::function&)>& select_devices, + bool test_auto_select, + const std::function& v2_session_checker, + bool disable_cpu_ep_fallback) { const auto run_test = [&](bool auto_select) { std::vector> inputs(1); auto& input = inputs.back(); @@ -215,7 +194,6 @@ void RunBasicTest(const std::string& ep_name, std::optionalGetNumHardwareDevices(*ort_env, &num_devices)); - if (num_devices == 0) { - return found; - } - - InlinedVector devices(num_devices); - Ort::ThrowOnError(c_api->GetHardwareDevices(*ort_env, devices.data(), num_devices)); - - for (const OrtHardwareDevice* device : devices) { - const OrtHardwareDeviceType type = c_api->HardwareDevice_Type(device); - found.has_npu |= type == OrtHardwareDeviceType_NPU; - found.has_gpu |= type == OrtHardwareDeviceType_GPU; - } - - return found; -} - -CoreMLDevices GetCoreMLEpDevices() { - CoreMLDevices found; - size_t num_npu = 0; - size_t num_gpu = 0; - const OrtApi* c_api = &Ort::GetApi(); - const OrtEpDevice* const* ep_devices = nullptr; - size_t num_devices = 0; - - Ort::ThrowOnError(c_api->GetEpDevices(*ort_env, &ep_devices, &num_devices)); - - for (size_t i = 0; i < num_devices; ++i) { - const OrtEpDevice* ep_device = ep_devices[i]; - if (strcmp(c_api->EpDevice_EpName(ep_device), kCoreMLExecutionProvider) != 0) { - continue; - } - - const OrtHardwareDeviceType type = c_api->HardwareDevice_Type(c_api->EpDevice_Device(ep_device)); - if (type == OrtHardwareDeviceType_NPU) { - ++num_npu; - if (found.npu == nullptr) { - found.npu = ep_device; - } - } else if (type == OrtHardwareDeviceType_GPU) { - ++num_gpu; - if (found.gpu == nullptr) { - found.gpu = ep_device; - } - } else { - ADD_FAILURE() << "CoreML EP advertised an OrtEpDevice with unexpected hardware device type " << type; - } - } - - // The factory may advertise at most one NPU and one GPU. This helper retains only the first OrtEpDevice of each - // type, so the separate counters ensure that any additional OrtEpDevice of the same type is detected. - EXPECT_LE(num_npu, 1u) << "CoreML EP advertised more than one NPU OrtEpDevice."; - EXPECT_LE(num_gpu, 1u) << "CoreML EP advertised more than one GPU OrtEpDevice."; - - return found; -} - -// Returns whether this machine has an NPU that the CoreML EP factory may advertise at the runtime Core ML version. -bool CoreMLCanClaimNpu(const HardwareDeviceTypes& hardware) { - return hardware.has_npu && coreml::util::CoreMLVersion() >= MINIMUM_COREML_VERSION_FOR_NEURAL_ENGINE_SELECTION; -} - -// Returns whether this machine has a GPU that the CoreML EP factory may advertise at the runtime Core ML version. -bool CoreMLCanClaimGpu(const HardwareDeviceTypes& hardware) { - return hardware.has_gpu && coreml::util::CoreMLVersion() >= MINIMUM_COREML_VERSION; -} - -// Verifies that a session for mul_1.onnx assigns its input to the CoreML EP. Used as the session checker for -// RunBasicTest, which runs the inference itself afterwards. -void AssertMul1InputAssignedToCoreML(Ort::Session& session) { - const OrtApi* c_api = &Ort::GetApi(); - const OrtEpDevice* input_ep_device = nullptr; - ASSERT_ORTSTATUS_OK(c_api->SessionGetEpDeviceForInputs(session, &input_ep_device, 1)); - ASSERT_NE(input_ep_device, nullptr); - ASSERT_STREQ(c_api->EpDevice_EpName(input_ep_device), kCoreMLExecutionProvider); -} - -// Verifies the input assignment and runs the inference. Used by the tests that create the session themselves. -void AssertMul1SessionRunsOnCoreML(Ort::Session& session) { - ASSERT_NO_FATAL_FAILURE(AssertMul1InputAssignedToCoreML(session)); - - std::vector> inputs(1); - auto& input = inputs.back(); - input.name = "X"; - input.dims = {3, 2}; - input.values = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f}; - - auto allocator = std::make_unique(); - RunSession(allocator.get(), session, inputs, "Y", - {3, 2}, {1.0f, 4.0f, 9.0f, 16.0f, 25.0f, 36.0f}, nullptr); -} -} // namespace - -TEST(AutoEpSelection, CoreMLEP) { - const HardwareDeviceTypes hardware = GetHardwareDeviceTypes(); - const bool can_claim_npu = CoreMLCanClaimNpu(hardware); - const bool can_claim_gpu = CoreMLCanClaimGpu(hardware); - const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); - - // Verify that the factory does not advertise devices rejected by its version requirements. - if (hardware.has_npu && !can_claim_npu) { - EXPECT_EQ(coreml_devices.npu, nullptr) << "CoreML EP must not advertise the NPU below Core ML 6."; - } - if (hardware.has_gpu && !can_claim_gpu) { - EXPECT_EQ(coreml_devices.gpu, nullptr) << "CoreML EP must not advertise devices below Core ML 5."; - } - - // Skip when the factory cannot advertise any discovered accelerator. For example, on iOS versions earlier - // than 16, discovery may report an NPU that cannot be advertised because NPU selection requires Core ML 6, - // while GPU discovery does not currently support iOS. - if (!can_claim_npu && !can_claim_gpu) { - GTEST_SKIP() << "No hardware device that the CoreML EP factory can advertise on this machine."; - } - - if (can_claim_npu) { - ASSERT_NE(coreml_devices.npu, nullptr) << "CoreML EP did not claim the NPU hardware device."; - } - if (can_claim_gpu) { - ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not claim the GPU hardware device."; - } - - // Verify the CoreML factory's vendor name and vendor ID, along with the hardware vendor ID. The factory reports - // "Microsoft", like the other internal factories: the factory vendor identifies who provides the EP - // implementation, not the hardware vendor. - // Apple device discovery currently assigns Apple's PCI vendor id (0x106B) to every NPU and GPU it reports. The - // CoreML factory intentionally accepts any discovered GPU regardless of vendor, so this hardware vendor ID - // assertion must be updated if discovery later reports a non-Apple GPU (see the multi-GPU TODO in - // core/platform/apple/device_discovery.cc). - const OrtApi* c_api = &Ort::GetApi(); - size_t num_ep_devices = 0; - const OrtEpDevice* const* all_ep_devices = nullptr; - ASSERT_ORTSTATUS_OK(c_api->GetEpDevices(*ort_env, &all_ep_devices, &num_ep_devices)); - - for (size_t i = 0; i < num_ep_devices; ++i) { - const OrtEpDevice* ep_device = all_ep_devices[i]; - if (strcmp(c_api->EpDevice_EpName(ep_device), kCoreMLExecutionProvider) != 0) { - continue; - } - - EXPECT_STREQ(c_api->EpDevice_EpVendor(ep_device), "Microsoft"); - EXPECT_EQ(c_api->HardwareDevice_VendorId(c_api->EpDevice_Device(ep_device)), uint32_t{0x106B}); - EXPECT_EQ(ep_device->ep_factory->GetVendorId(ep_device->ep_factory), OrtDevice::VendorIds::MICROSOFT); - } - - // Both the "test.ep_to_select" auto-selection path and the AppendExecutionProvider_V2 path select CoreML as - // the only non-CPU EP. Disabling CPU fallback makes session creation fail unless CoreML takes the entire graph. - // The V2 path also checks the input assignment directly. CoreMLEPPreferNpu and CoreMLEPPreferGpu perform the - // same assignment check for policy-based selection. - RunBasicTest(kCoreMLExecutionProvider, std::nullopt, Ort::KeyValuePairs{}, /*select_devices*/ nullptr, - /*test_auto_select*/ true, AssertMul1InputAssignedToCoreML, /*disable_cpu_ep_fallback*/ true); -} - -// Tests explicit device selection through AppendExecutionProvider_V2. Each test case selects one or more CoreML -// devices, optionally provides MLComputeUnits in ep_options, and verifies that the session assigns the graph to -// CoreML and runs successfully. Test cases are skipped individually only when a required device is unavailable. -// Test cases with MLComputeUnits verify that the caller may narrow accelerator use within the selected devices: -// for an NPU-and-GPU selection, CPUAndGPU enables only the GPU accelerator and CPUAndNeuralEngine enables only -// the NPU. CPUOnly disables all selected accelerators while the graph remains assigned to CoreML. -TEST(AutoEpSelection, CoreMLEPExplicitDeviceSelection) { - enum class Selection { kNpuAndGpu, - kGpuOnly, - kNpuOnly, - kEveryAdvertisedDevice }; - - struct TestCase { - const char* description; - Selection selection; - const char* compute_units; // nullptr: MLComputeUnits omitted - }; - - const TestCase test_cases[] = { - {"NPU and GPU, MLComputeUnits omitted", Selection::kNpuAndGpu, nullptr}, - {"NPU and GPU, CPUAndGPU", Selection::kNpuAndGpu, "CPUAndGPU"}, - {"NPU and GPU, CPUAndNeuralEngine", Selection::kNpuAndGpu, "CPUAndNeuralEngine"}, - {"GPU only, MLComputeUnits omitted", Selection::kGpuOnly, nullptr}, - {"NPU only, MLComputeUnits omitted", Selection::kNpuOnly, nullptr}, - {"every advertised device, CPUOnly", Selection::kEveryAdvertisedDevice, "CPUOnly"}, - }; - - const HardwareDeviceTypes hardware = GetHardwareDeviceTypes(); - const bool can_claim_npu = CoreMLCanClaimNpu(hardware); - const bool can_claim_gpu = CoreMLCanClaimGpu(hardware); - if (!can_claim_npu && !can_claim_gpu) { - GTEST_SKIP() << "No hardware device that the CoreML EP factory can advertise on this machine."; - } - - const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); - if (can_claim_npu) { - ASSERT_NE(coreml_devices.npu, nullptr) << "CoreML EP did not claim the NPU hardware device."; - } - if (can_claim_gpu) { - ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not claim the GPU hardware device."; - } - - for (const TestCase& test_case : test_cases) { - SCOPED_TRACE(test_case.description); - - const bool wants_npu = test_case.selection == Selection::kNpuAndGpu || - test_case.selection == Selection::kNpuOnly || - (test_case.selection == Selection::kEveryAdvertisedDevice && can_claim_npu); - const bool wants_gpu = test_case.selection == Selection::kNpuAndGpu || - test_case.selection == Selection::kGpuOnly || - (test_case.selection == Selection::kEveryAdvertisedDevice && can_claim_gpu); - if ((wants_npu && !can_claim_npu) || (wants_gpu && !can_claim_gpu)) { - continue; - } - - std::vector devices; - if (wants_npu) { - devices.push_back(coreml_devices.npu); - } - if (wants_gpu) { - devices.push_back(coreml_devices.gpu); - } - - Ort::KeyValuePairs ep_options; - if (test_case.compute_units != nullptr) { - ep_options.Add(kCoremlProviderOption_MLComputeUnits, test_case.compute_units); - } - - // Run only the AppendExecutionProvider_V2 path because it passes the test case's full device selection to the - // factory. The "test.ep_to_select" path would pass only the first CoreML device. Verify the input assignment and - // disable ORT CPU fallback so CoreML must execute the entire graph. - RunBasicTest( - kCoreMLExecutionProvider, std::nullopt, ep_options, - [&devices](std::vector& selected) { selected = devices; }, - /*test_auto_select*/ false, AssertMul1InputAssignedToCoreML, /*disable_cpu_ep_fallback*/ true); - } -} - -// PREFER_NPU must select the CoreML EP for the Apple Neural Engine instead of falling back to the ORT CPU EP. -// MAX_EFFICIENCY and MIN_OVERALL_POWER use the same selector as PREFER_NPU, so the test verifies the same result -// for both aliases. -TEST(AutoEpSelection, CoreMLEPPreferNpu) { - if (!CoreMLCanClaimNpu(GetHardwareDeviceTypes())) { - GTEST_SKIP() << "No NPU hardware device, or this Core ML version cannot advertise it."; - } - ASSERT_NE(GetCoreMLEpDevices().npu, nullptr) << "CoreML EP did not claim the NPU hardware device."; - - for (const OrtExecutionProviderDevicePolicy policy : {OrtExecutionProviderDevicePolicy_PREFER_NPU, - OrtExecutionProviderDevicePolicy_MAX_EFFICIENCY, - OrtExecutionProviderDevicePolicy_MIN_OVERALL_POWER}) { - SCOPED_TRACE(testing::Message() << "policy " << policy); - Ort::SessionOptions session_options; - session_options.SetEpSelectionPolicy(policy); - Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); - - ASSERT_NO_FATAL_FAILURE(AssertMul1SessionRunsOnCoreML(session)); - } -} - -// PREFER_GPU must select the CoreML EP. When the internal WebGPU EP is also registered, both EPs advertise an -// OrtEpDevice for the same Apple GPU, and the test verifies that CoreML is selected over WebGPU. -// Without the internal WebGPU EP, the test simply verifies that PREFER_GPU selects CoreML. -// MAX_PERFORMANCE uses the same selector and is covered by the same assertions. -TEST(AutoEpSelection, CoreMLEPPreferGpu) { - if (!CoreMLCanClaimGpu(GetHardwareDeviceTypes())) { - GTEST_SKIP() << "No GPU hardware device that the CoreML EP factory can advertise on this machine."; - } - - const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); - ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not claim the GPU hardware device."; - -#if defined(USE_WEBGPU) && !defined(ORT_USE_EP_API_ADAPTERS) - // Verify that WebGPU also advertises an OrtEpDevice for the same GPU. Otherwise, selecting CoreML would not show - // that it is preferred over WebGPU for the same hardware device. - bool matching_webgpu_device_present = false; - { - const OrtApi* c_api = &Ort::GetApi(); - const OrtHardwareDevice* coreml_gpu_hw = c_api->EpDevice_Device(coreml_devices.gpu); - const OrtEpDevice* const* ep_devices = nullptr; - size_t num_devices = 0; - - ASSERT_ORTSTATUS_OK(c_api->GetEpDevices(*ort_env, &ep_devices, &num_devices)); - for (size_t i = 0; i < num_devices; ++i) { - if (strcmp(c_api->EpDevice_EpName(ep_devices[i]), kWebGpuExecutionProvider) == 0 && - c_api->EpDevice_Device(ep_devices[i]) == coreml_gpu_hw) { - matching_webgpu_device_present = true; - break; - } - } - } - - ASSERT_TRUE(matching_webgpu_device_present) - << "Expected WebGPU to also advertise an OrtEpDevice for the CoreML GPU's hardware device in this build."; -#endif - - for (const OrtExecutionProviderDevicePolicy policy : {OrtExecutionProviderDevicePolicy_PREFER_GPU, - OrtExecutionProviderDevicePolicy_MAX_PERFORMANCE}) { - SCOPED_TRACE(testing::Message() << "policy " << policy); - Ort::SessionOptions session_options; - session_options.SetEpSelectionPolicy(policy); - Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); - - ASSERT_NO_FATAL_FAILURE(AssertMul1SessionRunsOnCoreML(session)); - } -} - -// MLComputeUnits must not enable an accelerator outside the selected devices. The policy path uses PREFER_NPU to -// select only the NPU, then tests CPUAndGPU and ALL. The AppendExecutionProvider_V2 path selects only the GPU, then -// tests CPUAndNeuralEngine and ALL. Each value must be rejected because it enables an unselected accelerator. -TEST(AutoEpSelection, CoreMLEPConflictingComputeUnitsRejected) { - const HardwareDeviceTypes hardware = GetHardwareDeviceTypes(); - const bool can_claim_npu = CoreMLCanClaimNpu(hardware); - const bool can_claim_gpu = CoreMLCanClaimGpu(hardware); - if (!can_claim_npu && !can_claim_gpu) { - GTEST_SKIP() << "No hardware device that the CoreML EP factory can advertise on this machine."; - } - - const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); - - const auto expect_rejected = [](Ort::SessionOptions& session_options, const char* explanation, - bool check_error_code) { - try { - Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); - FAIL() << "Expected session creation to fail: " << explanation; - } catch (const Ort::Exception& ex) { - if (check_error_code) { - EXPECT_EQ(ex.GetOrtErrorCode(), ORT_INVALID_ARGUMENT); - } - EXPECT_THAT(ex.what(), ::testing::HasSubstr("was not selected")); - } - }; - - if (can_claim_npu) { - ASSERT_NE(coreml_devices.npu, nullptr) << "CoreML EP did not claim the NPU hardware device."; - - for (const char* compute_units : {"CPUAndGPU", "ALL"}) { - SCOPED_TRACE(compute_units); - Ort::SessionOptions session_options; - session_options.SetEpSelectionPolicy(OrtExecutionProviderDevicePolicy_PREFER_NPU); - const std::string option_key = - OrtSessionOptions::GetProviderOptionPrefix(kCoreMLExecutionProvider) + kCoremlProviderOption_MLComputeUnits; - session_options.AddConfigEntry(option_key.c_str(), compute_units); - expect_rejected(session_options, "the value enables the GPU, which PREFER_NPU did not select.", - /*check_error_code*/ true); - } - } - - if (can_claim_gpu) { - ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not claim the GPU hardware device."; - - for (const char* compute_units : {"CPUAndNeuralEngine", "ALL"}) { - SCOPED_TRACE(compute_units); - Ort::KeyValuePairs ep_options; - ep_options.Add(kCoremlProviderOption_MLComputeUnits, compute_units); - std::vector ep_devices{Ort::ConstEpDevice{coreml_devices.gpu}}; - Ort::SessionOptions session_options; - session_options.AppendExecutionProvider_V2(*ort_env, ep_devices, ep_options); - // The V2 registration path currently reports the factory's ORT_INVALID_ARGUMENT as ORT_FAIL, so only the - // diagnostic message is verified here. The policy path above verifies ORT_INVALID_ARGUMENT. - expect_rejected(session_options, "the value enables the NPU, but only the GPU was selected.", - /*check_error_code*/ false); - } - } -} - -// The factory must reject selecting the same device type twice. AppendExecutionProvider_V2 validation -// checks only that all selected devices use the same EP and factory, so the duplicate reaches the factory-specific -// validation. -TEST(AutoEpSelection, CoreMLEPDuplicateDeviceSelectionRejected) { - const HardwareDeviceTypes hardware = GetHardwareDeviceTypes(); - if (!CoreMLCanClaimNpu(hardware) && !CoreMLCanClaimGpu(hardware)) { - GTEST_SKIP() << "No hardware device that the CoreML EP factory can advertise on this machine."; - } - - const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); - ASSERT_TRUE(coreml_devices.npu != nullptr || coreml_devices.gpu != nullptr) - << "CoreML EP did not advertise any supported NPU or GPU device."; - - // The factory checks for duplicate NPU and GPU selections separately, so test each available device type. - for (const OrtEpDevice* device : {coreml_devices.npu, coreml_devices.gpu}) { - if (device == nullptr) { - continue; - } - SCOPED_TRACE(testing::Message() << "device type " - << Ort::GetApi().HardwareDevice_Type(Ort::GetApi().EpDevice_Device(device))); - - std::vector ep_devices{Ort::ConstEpDevice{device}, Ort::ConstEpDevice{device}}; - Ort::SessionOptions session_options; - session_options.AppendExecutionProvider_V2(*ort_env, ep_devices, Ort::KeyValuePairs{}); - - try { - Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); - FAIL() << "Expected session creation to fail: the same CoreML device was selected twice."; - } catch (const Ort::Exception& ex) { - // The V2 registration path currently reports the factory's ORT_INVALID_ARGUMENT as ORT_FAIL, so this test - // verifies only the diagnostic message. - EXPECT_THAT(ex.what(), ::testing::HasSubstr("At most one device of each type can be selected")); - } - } -} - -// Verifies that an unknown MLComputeUnits value is not rejected by the factory but reaches CoreMLOptions, which -// rejects it during provider creation, so session creation fails with the CoreMLOptions diagnostic. -TEST(AutoEpSelection, CoreMLEPUnknownComputeUnitsRejectedByProvider) { - const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); - const OrtEpDevice* device = coreml_devices.npu != nullptr ? coreml_devices.npu : coreml_devices.gpu; - if (device == nullptr) { - GTEST_SKIP() << "No CoreML OrtEpDevice on this machine."; - } - - Ort::KeyValuePairs ep_options; - ep_options.Add(kCoremlProviderOption_MLComputeUnits, "NotAComputeUnitsValue"); - std::vector ep_devices{Ort::ConstEpDevice{device}}; - - Ort::SessionOptions session_options; - session_options.AppendExecutionProvider_V2(*ort_env, ep_devices, ep_options); - - try { - Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); - FAIL() << "Expected session creation to fail: MLComputeUnits=NotAComputeUnitsValue is not a valid value."; - } catch (const Ort::Exception& ex) { - EXPECT_THAT(ex.what(), ::testing::HasSubstr("Invalid value for option")); - EXPECT_THAT(ex.what(), ::testing::HasSubstr("NotAComputeUnitsValue")); - } -} - -// The two tests below call CoreMLEpFactory through its EpFactoryInternal wrapper using synthetic hardware devices. -// Device advertising depends only on the device type and runtime Core ML version, so the tests require no physical -// accelerator and can run on Intel Macs that meet the version requirement. Any published OrtEpDevice instances are -// released through the EP API. -namespace { - -// Creates a synthetic hardware device of the given type. The device's vendor does not affect whether the factory -// advertises the device. The Apple values below match those reported by device discovery. -OrtHardwareDevice MakeSyntheticHardwareDevice(OrtHardwareDeviceType type) { - OrtHardwareDevice device{}; - device.type = type; - device.vendor_id = 0x106B; // Apple's PCI vendor ID - device.vendor = "Apple"; - return device; -} - -// Number of calls to the injected GetVersion callback. -int g_injected_version_calls = 0; - -} // namespace - -// Verifies that GetSupportedDevices preserves input order, advertises at most one NPU and one GPU, ignores -// duplicate devices of either type, and excludes CPU devices. -// It publishes no more than max_ep_devices devices and leaves the remaining output entries unchanged. -// Every successful call writes the output count, including zero when no devices are advertised. -TEST(AutoEpSelection, CoreMLEPGetSupportedDevicesEnumeration) { - if (coreml::util::CoreMLVersion() < MINIMUM_COREML_VERSION_FOR_NEURAL_ENGINE_SELECTION) { - GTEST_SKIP() << "This test requires Core ML 6 or later because it expects the factory to advertise an NPU."; - } - - EpFactoryInternal factory{std::make_unique()}; - OrtEpFactory* c_factory = &factory; - ASSERT_NE(c_factory->GetSupportedDevices, nullptr); - - const OrtHardwareDevice gpu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_GPU); - const OrtHardwareDevice npu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_NPU); - const OrtHardwareDevice second_gpu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_GPU); - const OrtHardwareDevice second_npu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_NPU); - const OrtHardwareDevice cpu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_CPU); - - constexpr size_t kOutputCapacity = 5; - OrtEpDevice untouched_marker{}; - - // Calls GetSupportedDevices on the given hardware devices with max_ep_devices as the output capacity. Returns the - // hardware device behind each published OrtEpDevice in publication order and releases the instances. Output - // entries beyond the published count must keep their marker value. - const auto published_hardware = [&](gsl::span hardware, size_t max_ep_devices) { - std::array ep_devices{}; - ep_devices.fill(&untouched_marker); - size_t num_ep_devices = 42; // a successful call must overwrite this - Ort::Status status{c_factory->GetSupportedDevices(c_factory, hardware.data(), hardware.size(), - ep_devices.data(), max_ep_devices, &num_ep_devices)}; - - InlinedVector result; - EXPECT_TRUE(status.IsOK()) << status.GetErrorMessage(); - EXPECT_LE(num_ep_devices, max_ep_devices); - if (!status.IsOK() || num_ep_devices > max_ep_devices) { - return result; - } - - for (size_t i = 0; i < ep_devices.size(); ++i) { - if (i >= num_ep_devices) { - EXPECT_EQ(ep_devices[i], &untouched_marker) << "Entry " << i << " lies beyond the published count."; - continue; - } - - EXPECT_NE(ep_devices[i], &untouched_marker) << "Published entry " << i << " was not written."; - EXPECT_NE(ep_devices[i], nullptr) << "Published entry " << i << " is null."; - if (ep_devices[i] == nullptr || ep_devices[i] == &untouched_marker) { - continue; - } - - result.push_back(Ort::GetApi().EpDevice_Device(ep_devices[i])); - Ort::GetEpApi().ReleaseEpDevice(ep_devices[i]); - } - - return result; - }; - - // Only the first GPU and NPU are advertised, in that order. - const std::array gpu_first{&gpu, &npu, &second_gpu, &second_npu, &cpu}; - EXPECT_THAT(published_hardware(gpu_first, kOutputCapacity), ::testing::ElementsAre(&gpu, &npu)); - - // Skip the leading CPU device and preserve the NPU-before-GPU order. - const std::array cpu_first{&cpu, &npu, &gpu}; - EXPECT_THAT(published_hardware(cpu_first, kOutputCapacity), ::testing::ElementsAre(&npu, &gpu)); - - // The output capacity limits the total number of published devices. A capacity of 1 keeps only the first - // supported device. A capacity of 0 produces no devices. - EXPECT_THAT(published_hardware(gpu_first, 1), ::testing::ElementsAre(&gpu)); - EXPECT_THAT(published_hardware(gpu_first, 0), ::testing::IsEmpty()); - - // When the input contains only a CPU device, no devices are published and the output count is set to zero. - const std::array cpu_only{&cpu}; - EXPECT_THAT(published_hardware(cpu_only, kOutputCapacity), ::testing::IsEmpty()); -} - -// If the second OrtEpDevice creation fails, GetSupportedDevices must return an error, release the first device, -// and leave the output array and count unchanged, following ORT's C API convention for failed calls. -// The test replaces the GetVersion callback on EpFactoryInternal with one that returns a valid version on the -// first call and an invalid version on the second. CreateEpDevice validates each version, causing the second -// creation to fail. -TEST(AutoEpSelection, CoreMLEPGetSupportedDevicesRollsBackOnCreateFailure) { - if (coreml::util::CoreMLVersion() < MINIMUM_COREML_VERSION_FOR_NEURAL_ENGINE_SELECTION) { - GTEST_SKIP() << "This test requires Core ML 6 or later so the factory can advertise both an NPU and a GPU."; - } - - EpFactoryInternal factory{std::make_unique()}; - OrtEpFactory* c_factory = &factory; - g_injected_version_calls = 0; - c_factory->GetVersion = [](const OrtEpFactory*) noexcept -> const char* { - // A valid version for the first device and an invalid one for the second, so the second CreateEpDevice fails. - return ++g_injected_version_calls == 1 ? "1.0.0" : "not a version"; - }; - - const OrtHardwareDevice npu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_NPU); - const OrtHardwareDevice gpu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_GPU); - const std::array hardware{&npu, &gpu}; - - OrtEpDevice untouched_marker{}; - std::array ep_devices{&untouched_marker, &untouched_marker}; - size_t num_ep_devices = 42; - Ort::Status status{c_factory->GetSupportedDevices(c_factory, hardware.data(), hardware.size(), ep_devices.data(), - ep_devices.size(), &num_ep_devices)}; - - ASSERT_FALSE(status.IsOK()) - << "GetSupportedDevices succeeded although the second CreateEpDevice was made to fail."; - EXPECT_EQ(g_injected_version_calls, 2) - << "GetVersion must be called for both device creation attempts."; - EXPECT_THAT(ep_devices, ::testing::Each(&untouched_marker)); - EXPECT_EQ(num_ep_devices, size_t{42}); -} - -#endif // defined(USE_COREML) && defined(__APPLE__) - -#if defined(USE_COREML) && !defined(__APPLE__) -// CoreML can be built on non-Apple platforms to test model conversion, but it cannot execute models there. -// The factory checks only the device type and Core ML version. Because the non-Apple CoreMLVersion stub reports -// Core ML 7, the factory would accept accelerators discovered on a non-Apple system. Verify that the platform guards -// prevent the CoreML factory from being registered on non-Apple builds. -// Registration is checked with GetHardwareDeviceEpIncompatibilityDetails rather than through the published device -// list. The function looks up the factory by EP name before checking hardware compatibility, so any discovered -// device, including the CPU, is sufficient and the test does not depend on accelerator discovery. -TEST(AutoEpSelection, CoreMLEPIsNotRegisteredOnNonApplePlatforms) { - const OrtApi* c_api = &Ort::GetApi(); - - size_t num_devices = 0; - ASSERT_ORTSTATUS_OK(c_api->GetNumHardwareDevices(*ort_env, &num_devices)); - ASSERT_GT(num_devices, 0u) << "Expected device discovery to report at least the CPU device."; - - InlinedVector devices(num_devices); - ASSERT_ORTSTATUS_OK(c_api->GetHardwareDevices(*ort_env, devices.data(), num_devices)); - - OrtDeviceEpIncompatibilityDetails* details = nullptr; - Ort::Status status{c_api->GetHardwareDeviceEpIncompatibilityDetails(*ort_env, kCoreMLExecutionProvider, - devices.front(), &details)}; - // If the CoreML factory is unexpectedly registered, the call succeeds and returns an object that must be released - // before the test reports the failure. - if (details != nullptr) { - c_api->ReleaseDeviceEpIncompatibilityDetails(details); - } - - ASSERT_FALSE(status.IsOK()) << "A factory named " << kCoreMLExecutionProvider << " is registered on a non-Apple " - << "platform, where CoreML cannot execute models."; - EXPECT_EQ(status.GetErrorCode(), ORT_INVALID_ARGUMENT); - // ORT_INVALID_ARGUMENT alone does not prove that the factory is absent. If the EP name matches a registered - // factory, GetHardwareDeviceEpIncompatibilityDetails invokes the factory's compatibility callback, - // which may return the same error code. Verify the diagnostic to confirm that no factory matched the CoreML EP name. - EXPECT_THAT(status.GetErrorMessage(), ::testing::HasSubstr("No valid factory found for execution provider")); -} -#endif // defined(USE_COREML) && !defined(__APPLE__) - // tests for AutoEP selection related things in the API that aren't covered by the other tests. TEST(AutoEpSelection, MiscApiTests) { const OrtApi* c_api = &Ort::GetApi(); diff --git a/onnxruntime/test/autoep/test_selection_coreml.cc b/onnxruntime/test/autoep/test_selection_coreml.cc new file mode 100644 index 0000000000000..abf49a09373c9 --- /dev/null +++ b/onnxruntime/test/autoep/test_selection_coreml.cc @@ -0,0 +1,660 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// AutoEP selection tests for the CoreML EP factory. They use shared onnxruntime_autoep_test infrastructure, +// including ort_env and RunBasicTest. + +#if defined(USE_COREML) + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "core/common/inlined_containers.h" +#include "core/graph/constants.h" +#include "core/session/onnxruntime_cxx_api.h" + +#if defined(__APPLE__) +#include "core/framework/ortdevice.h" +#include "core/providers/coreml/coreml_provider_factory.h" +#include "core/providers/coreml/model/host_utils.h" +#include "core/session/abi_devices.h" +#include "core/session/abi_session_options_impl.h" +#include "core/session/plugin_ep/ep_factory_coreml.h" +#include "core/session/plugin_ep/ep_factory_internal.h" +#endif + +#include "test_allocator.h" +#include "test/autoep/test_autoep_utils.h" +#include "test/shared_lib/utils.h" +#include "test/util/include/api_asserts.h" + +extern std::unique_ptr ort_env; + +namespace onnxruntime { +namespace test { + +#if defined(__APPLE__) +namespace { +// The CoreML EP factory creates OrtEpDevice instances only for claimable NPU and GPU hardware. The tests use +// GetHardwareDevices and the runtime Core ML version to determine which devices the factory should advertise, +// rather than relying on GetEpDevices itself. A device-dependent test skips only if the required hardware is +// unavailable or excluded by the Core ML version check. Otherwise, the test fails if the factory is not registered +// or does not return the expected OrtEpDevice. The tests also verify that no devices are advertised below Core ML 5 +// and that no NPU is advertised below Core ML 6. +struct CoreMLDevices { + const OrtEpDevice* npu = nullptr; + const OrtEpDevice* gpu = nullptr; +}; + +// Hardware device types reported by device discovery, independent of EP support. +struct HardwareDeviceTypes { + bool has_npu = false; + bool has_gpu = false; +}; + +HardwareDeviceTypes GetHardwareDeviceTypes() { + HardwareDeviceTypes found; + const OrtApi* c_api = &Ort::GetApi(); + size_t num_devices = 0; + + Ort::ThrowOnError(c_api->GetNumHardwareDevices(*ort_env, &num_devices)); + if (num_devices == 0) { + return found; + } + + InlinedVector devices(num_devices); + Ort::ThrowOnError(c_api->GetHardwareDevices(*ort_env, devices.data(), num_devices)); + + for (const OrtHardwareDevice* device : devices) { + const OrtHardwareDeviceType type = c_api->HardwareDevice_Type(device); + found.has_npu |= type == OrtHardwareDeviceType_NPU; + found.has_gpu |= type == OrtHardwareDeviceType_GPU; + } + + return found; +} + +CoreMLDevices GetCoreMLEpDevices() { + CoreMLDevices found; + size_t num_npu = 0; + size_t num_gpu = 0; + const OrtApi* c_api = &Ort::GetApi(); + const OrtEpDevice* const* ep_devices = nullptr; + size_t num_devices = 0; + + Ort::ThrowOnError(c_api->GetEpDevices(*ort_env, &ep_devices, &num_devices)); + + for (size_t i = 0; i < num_devices; ++i) { + const OrtEpDevice* ep_device = ep_devices[i]; + if (strcmp(c_api->EpDevice_EpName(ep_device), kCoreMLExecutionProvider) != 0) { + continue; + } + + const OrtHardwareDeviceType type = c_api->HardwareDevice_Type(c_api->EpDevice_Device(ep_device)); + if (type == OrtHardwareDeviceType_NPU) { + ++num_npu; + if (found.npu == nullptr) { + found.npu = ep_device; + } + } else if (type == OrtHardwareDeviceType_GPU) { + ++num_gpu; + if (found.gpu == nullptr) { + found.gpu = ep_device; + } + } else { + ADD_FAILURE() << "CoreML EP advertised an OrtEpDevice with unexpected hardware device type " << type; + } + } + + // The factory may advertise at most one NPU and one GPU. This helper retains only the first OrtEpDevice of each + // type, so the separate counters ensure that any additional OrtEpDevice of the same type is detected. + EXPECT_LE(num_npu, 1u) << "CoreML EP advertised more than one NPU OrtEpDevice."; + EXPECT_LE(num_gpu, 1u) << "CoreML EP advertised more than one GPU OrtEpDevice."; + + return found; +} + +// Returns whether this machine has an NPU that the CoreML EP factory may advertise at the runtime Core ML version. +bool CoreMLCanClaimNpu(const HardwareDeviceTypes& hardware) { + return hardware.has_npu && coreml::util::CoreMLVersion() >= MINIMUM_COREML_VERSION_FOR_NEURAL_ENGINE_SELECTION; +} + +// Returns whether this machine has a GPU that the CoreML EP factory may advertise at the runtime Core ML version. +bool CoreMLCanClaimGpu(const HardwareDeviceTypes& hardware) { + return hardware.has_gpu && coreml::util::CoreMLVersion() >= MINIMUM_COREML_VERSION; +} + +// Verifies that a session for mul_1.onnx assigns its input to the CoreML EP. Used as the session checker for +// RunBasicTest, which runs the inference itself afterwards. +void AssertMul1InputAssignedToCoreML(Ort::Session& session) { + const OrtApi* c_api = &Ort::GetApi(); + const OrtEpDevice* input_ep_device = nullptr; + ASSERT_ORTSTATUS_OK(c_api->SessionGetEpDeviceForInputs(session, &input_ep_device, 1)); + ASSERT_NE(input_ep_device, nullptr); + ASSERT_STREQ(c_api->EpDevice_EpName(input_ep_device), kCoreMLExecutionProvider); +} + +// Verifies the input assignment and runs the inference. Used by the tests that create the session themselves. +void AssertMul1SessionRunsOnCoreML(Ort::Session& session) { + ASSERT_NO_FATAL_FAILURE(AssertMul1InputAssignedToCoreML(session)); + + std::vector> inputs(1); + auto& input = inputs.back(); + input.name = "X"; + input.dims = {3, 2}; + input.values = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f}; + + auto allocator = std::make_unique(); + RunSession(allocator.get(), session, inputs, "Y", + {3, 2}, {1.0f, 4.0f, 9.0f, 16.0f, 25.0f, 36.0f}, nullptr); +} +} // namespace + +TEST(AutoEpSelection, CoreMLEP) { + const HardwareDeviceTypes hardware = GetHardwareDeviceTypes(); + const bool can_claim_npu = CoreMLCanClaimNpu(hardware); + const bool can_claim_gpu = CoreMLCanClaimGpu(hardware); + const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); + + // Verify that the factory does not advertise devices rejected by its version requirements. + if (hardware.has_npu && !can_claim_npu) { + EXPECT_EQ(coreml_devices.npu, nullptr) << "CoreML EP must not advertise the NPU below Core ML 6."; + } + if (hardware.has_gpu && !can_claim_gpu) { + EXPECT_EQ(coreml_devices.gpu, nullptr) << "CoreML EP must not advertise devices below Core ML 5."; + } + + // Skip when the factory cannot advertise any discovered accelerator. For example, on iOS versions earlier + // than 16, discovery may report an NPU that cannot be advertised because NPU selection requires Core ML 6, + // while GPU discovery does not currently support iOS. + if (!can_claim_npu && !can_claim_gpu) { + GTEST_SKIP() << "No hardware device that the CoreML EP factory can advertise on this machine."; + } + + if (can_claim_npu) { + ASSERT_NE(coreml_devices.npu, nullptr) << "CoreML EP did not claim the NPU hardware device."; + } + if (can_claim_gpu) { + ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not claim the GPU hardware device."; + } + + // Verify the CoreML factory's vendor name and vendor ID, along with the hardware vendor ID. The factory reports + // "Microsoft", like the other internal factories: the factory vendor identifies who provides the EP + // implementation, not the hardware vendor. + // Apple device discovery currently assigns Apple's PCI vendor id (0x106B) to every NPU and GPU it reports. The + // CoreML factory intentionally accepts any discovered GPU regardless of vendor, so this hardware vendor ID + // assertion must be updated if discovery later reports a non-Apple GPU (see the multi-GPU TODO in + // core/platform/apple/device_discovery.cc). + const OrtApi* c_api = &Ort::GetApi(); + size_t num_ep_devices = 0; + const OrtEpDevice* const* all_ep_devices = nullptr; + ASSERT_ORTSTATUS_OK(c_api->GetEpDevices(*ort_env, &all_ep_devices, &num_ep_devices)); + + for (size_t i = 0; i < num_ep_devices; ++i) { + const OrtEpDevice* ep_device = all_ep_devices[i]; + if (strcmp(c_api->EpDevice_EpName(ep_device), kCoreMLExecutionProvider) != 0) { + continue; + } + + EXPECT_STREQ(c_api->EpDevice_EpVendor(ep_device), "Microsoft"); + EXPECT_EQ(c_api->HardwareDevice_VendorId(c_api->EpDevice_Device(ep_device)), uint32_t{0x106B}); + EXPECT_EQ(ep_device->ep_factory->GetVendorId(ep_device->ep_factory), OrtDevice::VendorIds::MICROSOFT); + } + + // Both the "test.ep_to_select" auto-selection path and the AppendExecutionProvider_V2 path select CoreML as + // the only non-CPU EP. Disabling CPU fallback makes session creation fail unless CoreML takes the entire graph. + // The V2 path also checks the input assignment directly. CoreMLEPPreferNpu and CoreMLEPPreferGpu perform the + // same assignment check for policy-based selection. + RunBasicTest(kCoreMLExecutionProvider, std::nullopt, Ort::KeyValuePairs{}, /*select_devices*/ nullptr, + /*test_auto_select*/ true, AssertMul1InputAssignedToCoreML, /*disable_cpu_ep_fallback*/ true); +} + +// Tests explicit device selection through AppendExecutionProvider_V2. Each test case selects one or more CoreML +// devices, optionally provides MLComputeUnits in ep_options, and verifies that the session assigns the graph to +// CoreML and runs successfully. Test cases are skipped individually only when a required device is unavailable. +// Test cases with MLComputeUnits verify that the caller may narrow accelerator use within the selected devices: +// for an NPU-and-GPU selection, CPUAndGPU enables only the GPU accelerator and CPUAndNeuralEngine enables only +// the NPU. CPUOnly disables all selected accelerators while the graph remains assigned to CoreML. +TEST(AutoEpSelection, CoreMLEPExplicitDeviceSelection) { + enum class Selection { kNpuAndGpu, + kGpuOnly, + kNpuOnly, + kEveryAdvertisedDevice }; + + struct TestCase { + const char* description; + Selection selection; + const char* compute_units; // nullptr: MLComputeUnits omitted + }; + + const TestCase test_cases[] = { + {"NPU and GPU, MLComputeUnits omitted", Selection::kNpuAndGpu, nullptr}, + {"NPU and GPU, CPUAndGPU", Selection::kNpuAndGpu, "CPUAndGPU"}, + {"NPU and GPU, CPUAndNeuralEngine", Selection::kNpuAndGpu, "CPUAndNeuralEngine"}, + {"GPU only, MLComputeUnits omitted", Selection::kGpuOnly, nullptr}, + {"NPU only, MLComputeUnits omitted", Selection::kNpuOnly, nullptr}, + {"every advertised device, CPUOnly", Selection::kEveryAdvertisedDevice, "CPUOnly"}, + }; + + const HardwareDeviceTypes hardware = GetHardwareDeviceTypes(); + const bool can_claim_npu = CoreMLCanClaimNpu(hardware); + const bool can_claim_gpu = CoreMLCanClaimGpu(hardware); + if (!can_claim_npu && !can_claim_gpu) { + GTEST_SKIP() << "No hardware device that the CoreML EP factory can advertise on this machine."; + } + + const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); + if (can_claim_npu) { + ASSERT_NE(coreml_devices.npu, nullptr) << "CoreML EP did not claim the NPU hardware device."; + } + if (can_claim_gpu) { + ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not claim the GPU hardware device."; + } + + for (const TestCase& test_case : test_cases) { + SCOPED_TRACE(test_case.description); + + const bool wants_npu = test_case.selection == Selection::kNpuAndGpu || + test_case.selection == Selection::kNpuOnly || + (test_case.selection == Selection::kEveryAdvertisedDevice && can_claim_npu); + const bool wants_gpu = test_case.selection == Selection::kNpuAndGpu || + test_case.selection == Selection::kGpuOnly || + (test_case.selection == Selection::kEveryAdvertisedDevice && can_claim_gpu); + if ((wants_npu && !can_claim_npu) || (wants_gpu && !can_claim_gpu)) { + continue; + } + + std::vector devices; + if (wants_npu) { + devices.push_back(coreml_devices.npu); + } + if (wants_gpu) { + devices.push_back(coreml_devices.gpu); + } + + Ort::KeyValuePairs ep_options; + if (test_case.compute_units != nullptr) { + ep_options.Add(kCoremlProviderOption_MLComputeUnits, test_case.compute_units); + } + + // Run only the AppendExecutionProvider_V2 path because it passes the test case's full device selection to the + // factory. The "test.ep_to_select" path would pass only the first CoreML device. Verify the input assignment and + // disable ORT CPU fallback so CoreML must execute the entire graph. + RunBasicTest( + kCoreMLExecutionProvider, std::nullopt, ep_options, + [&devices](std::vector& selected) { selected = devices; }, + /*test_auto_select*/ false, AssertMul1InputAssignedToCoreML, /*disable_cpu_ep_fallback*/ true); + } +} + +// PREFER_NPU must select the CoreML EP for the Apple Neural Engine instead of falling back to the ORT CPU EP. +// MAX_EFFICIENCY and MIN_OVERALL_POWER use the same selector as PREFER_NPU, so the test verifies the same result +// for both aliases. +TEST(AutoEpSelection, CoreMLEPPreferNpu) { + if (!CoreMLCanClaimNpu(GetHardwareDeviceTypes())) { + GTEST_SKIP() << "No NPU hardware device, or this Core ML version cannot advertise it."; + } + ASSERT_NE(GetCoreMLEpDevices().npu, nullptr) << "CoreML EP did not claim the NPU hardware device."; + + for (const OrtExecutionProviderDevicePolicy policy : {OrtExecutionProviderDevicePolicy_PREFER_NPU, + OrtExecutionProviderDevicePolicy_MAX_EFFICIENCY, + OrtExecutionProviderDevicePolicy_MIN_OVERALL_POWER}) { + SCOPED_TRACE(testing::Message() << "policy " << policy); + Ort::SessionOptions session_options; + session_options.SetEpSelectionPolicy(policy); + Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); + + ASSERT_NO_FATAL_FAILURE(AssertMul1SessionRunsOnCoreML(session)); + } +} + +// PREFER_GPU must select the CoreML EP. When the internal WebGPU EP is also registered, both EPs advertise an +// OrtEpDevice for the same Apple GPU, and the test verifies that CoreML is selected over WebGPU. +// Without the internal WebGPU EP, the test simply verifies that PREFER_GPU selects CoreML. +// MAX_PERFORMANCE uses the same selector and is covered by the same assertions. +TEST(AutoEpSelection, CoreMLEPPreferGpu) { + if (!CoreMLCanClaimGpu(GetHardwareDeviceTypes())) { + GTEST_SKIP() << "No GPU hardware device that the CoreML EP factory can advertise on this machine."; + } + + const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); + ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not claim the GPU hardware device."; + +#if defined(USE_WEBGPU) && !defined(ORT_USE_EP_API_ADAPTERS) + // Verify that WebGPU also advertises an OrtEpDevice for the same GPU. Otherwise, selecting CoreML would not show + // that it is preferred over WebGPU for the same hardware device. + bool matching_webgpu_device_present = false; + { + const OrtApi* c_api = &Ort::GetApi(); + const OrtHardwareDevice* coreml_gpu_hw = c_api->EpDevice_Device(coreml_devices.gpu); + const OrtEpDevice* const* ep_devices = nullptr; + size_t num_devices = 0; + + ASSERT_ORTSTATUS_OK(c_api->GetEpDevices(*ort_env, &ep_devices, &num_devices)); + for (size_t i = 0; i < num_devices; ++i) { + if (strcmp(c_api->EpDevice_EpName(ep_devices[i]), kWebGpuExecutionProvider) == 0 && + c_api->EpDevice_Device(ep_devices[i]) == coreml_gpu_hw) { + matching_webgpu_device_present = true; + break; + } + } + } + + ASSERT_TRUE(matching_webgpu_device_present) + << "Expected WebGPU to also advertise an OrtEpDevice for the CoreML GPU's hardware device in this build."; +#endif + + for (const OrtExecutionProviderDevicePolicy policy : {OrtExecutionProviderDevicePolicy_PREFER_GPU, + OrtExecutionProviderDevicePolicy_MAX_PERFORMANCE}) { + SCOPED_TRACE(testing::Message() << "policy " << policy); + Ort::SessionOptions session_options; + session_options.SetEpSelectionPolicy(policy); + Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); + + ASSERT_NO_FATAL_FAILURE(AssertMul1SessionRunsOnCoreML(session)); + } +} + +// MLComputeUnits must not enable an accelerator outside the selected devices. The policy path uses PREFER_NPU to +// select only the NPU, then tests CPUAndGPU and ALL. The AppendExecutionProvider_V2 path selects only the GPU, then +// tests CPUAndNeuralEngine and ALL. Each value must be rejected because it enables an unselected accelerator. +TEST(AutoEpSelection, CoreMLEPConflictingComputeUnitsRejected) { + const HardwareDeviceTypes hardware = GetHardwareDeviceTypes(); + const bool can_claim_npu = CoreMLCanClaimNpu(hardware); + const bool can_claim_gpu = CoreMLCanClaimGpu(hardware); + if (!can_claim_npu && !can_claim_gpu) { + GTEST_SKIP() << "No hardware device that the CoreML EP factory can advertise on this machine."; + } + + const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); + + const auto expect_rejected = [](Ort::SessionOptions& session_options, const char* explanation, + bool check_error_code) { + try { + Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); + FAIL() << "Expected session creation to fail: " << explanation; + } catch (const Ort::Exception& ex) { + if (check_error_code) { + EXPECT_EQ(ex.GetOrtErrorCode(), ORT_INVALID_ARGUMENT); + } + EXPECT_THAT(ex.what(), ::testing::HasSubstr("was not selected")); + } + }; + + if (can_claim_npu) { + ASSERT_NE(coreml_devices.npu, nullptr) << "CoreML EP did not claim the NPU hardware device."; + + for (const char* compute_units : {"CPUAndGPU", "ALL"}) { + SCOPED_TRACE(compute_units); + Ort::SessionOptions session_options; + session_options.SetEpSelectionPolicy(OrtExecutionProviderDevicePolicy_PREFER_NPU); + const std::string option_key = + OrtSessionOptions::GetProviderOptionPrefix(kCoreMLExecutionProvider) + kCoremlProviderOption_MLComputeUnits; + session_options.AddConfigEntry(option_key.c_str(), compute_units); + expect_rejected(session_options, "the value enables the GPU, which PREFER_NPU did not select.", + /*check_error_code*/ true); + } + } + + if (can_claim_gpu) { + ASSERT_NE(coreml_devices.gpu, nullptr) << "CoreML EP did not claim the GPU hardware device."; + + for (const char* compute_units : {"CPUAndNeuralEngine", "ALL"}) { + SCOPED_TRACE(compute_units); + Ort::KeyValuePairs ep_options; + ep_options.Add(kCoremlProviderOption_MLComputeUnits, compute_units); + std::vector ep_devices{Ort::ConstEpDevice{coreml_devices.gpu}}; + Ort::SessionOptions session_options; + session_options.AppendExecutionProvider_V2(*ort_env, ep_devices, ep_options); + // The V2 registration path currently reports the factory's ORT_INVALID_ARGUMENT as ORT_FAIL, so only the + // diagnostic message is verified here. The policy path above verifies ORT_INVALID_ARGUMENT. + expect_rejected(session_options, "the value enables the NPU, but only the GPU was selected.", + /*check_error_code*/ false); + } + } +} + +// The factory must reject selecting the same device type twice. AppendExecutionProvider_V2 validation +// checks only that all selected devices use the same EP and factory, so the duplicate reaches the factory-specific +// validation. +TEST(AutoEpSelection, CoreMLEPDuplicateDeviceSelectionRejected) { + const HardwareDeviceTypes hardware = GetHardwareDeviceTypes(); + if (!CoreMLCanClaimNpu(hardware) && !CoreMLCanClaimGpu(hardware)) { + GTEST_SKIP() << "No hardware device that the CoreML EP factory can advertise on this machine."; + } + + const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); + ASSERT_TRUE(coreml_devices.npu != nullptr || coreml_devices.gpu != nullptr) + << "CoreML EP did not advertise any supported NPU or GPU device."; + + // The factory checks for duplicate NPU and GPU selections separately, so test each available device type. + for (const OrtEpDevice* device : {coreml_devices.npu, coreml_devices.gpu}) { + if (device == nullptr) { + continue; + } + SCOPED_TRACE(testing::Message() << "device type " + << Ort::GetApi().HardwareDevice_Type(Ort::GetApi().EpDevice_Device(device))); + + std::vector ep_devices{Ort::ConstEpDevice{device}, Ort::ConstEpDevice{device}}; + Ort::SessionOptions session_options; + session_options.AppendExecutionProvider_V2(*ort_env, ep_devices, Ort::KeyValuePairs{}); + + try { + Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); + FAIL() << "Expected session creation to fail: the same CoreML device was selected twice."; + } catch (const Ort::Exception& ex) { + // The V2 registration path currently reports the factory's ORT_INVALID_ARGUMENT as ORT_FAIL, so this test + // verifies only the diagnostic message. + EXPECT_THAT(ex.what(), ::testing::HasSubstr("At most one device of each type can be selected")); + } + } +} + +// Verifies that an unknown MLComputeUnits value is not rejected by the factory but reaches CoreMLOptions, which +// rejects it during provider creation, so session creation fails with the CoreMLOptions diagnostic. +TEST(AutoEpSelection, CoreMLEPUnknownComputeUnitsRejectedByProvider) { + const CoreMLDevices coreml_devices = GetCoreMLEpDevices(); + const OrtEpDevice* device = coreml_devices.npu != nullptr ? coreml_devices.npu : coreml_devices.gpu; + if (device == nullptr) { + GTEST_SKIP() << "No CoreML OrtEpDevice on this machine."; + } + + Ort::KeyValuePairs ep_options; + ep_options.Add(kCoremlProviderOption_MLComputeUnits, "NotAComputeUnitsValue"); + std::vector ep_devices{Ort::ConstEpDevice{device}}; + + Ort::SessionOptions session_options; + session_options.AppendExecutionProvider_V2(*ort_env, ep_devices, ep_options); + + try { + Ort::Session session(*ort_env, ORT_TSTR("testdata/mul_1.onnx"), session_options); + FAIL() << "Expected session creation to fail: MLComputeUnits=NotAComputeUnitsValue is not a valid value."; + } catch (const Ort::Exception& ex) { + EXPECT_THAT(ex.what(), ::testing::HasSubstr("Invalid value for option")); + EXPECT_THAT(ex.what(), ::testing::HasSubstr("NotAComputeUnitsValue")); + } +} + +// The two tests below call CoreMLEpFactory through its EpFactoryInternal wrapper using synthetic hardware devices. +// Device advertising depends only on the device type and runtime Core ML version, so the tests require no physical +// accelerator and can run on Intel Macs that meet the version requirement. Any published OrtEpDevice instances are +// released through the EP API. +namespace { + +// Creates a synthetic hardware device of the given type. The device's vendor does not affect whether the factory +// advertises the device. The Apple values below match those reported by device discovery. +OrtHardwareDevice MakeSyntheticHardwareDevice(OrtHardwareDeviceType type) { + OrtHardwareDevice device{}; + device.type = type; + device.vendor_id = 0x106B; // Apple's PCI vendor ID + device.vendor = "Apple"; + return device; +} + +// Number of calls to the injected GetVersion callback. +int g_injected_version_calls = 0; + +} // namespace + +// Verifies that GetSupportedDevices preserves input order, advertises at most one NPU and one GPU, ignores +// duplicate devices of either type, and excludes CPU devices. +// It publishes no more than max_ep_devices devices and leaves the remaining output entries unchanged. +// Every successful call writes the output count, including zero when no devices are advertised. +TEST(AutoEpSelection, CoreMLEPGetSupportedDevicesEnumeration) { + if (coreml::util::CoreMLVersion() < MINIMUM_COREML_VERSION_FOR_NEURAL_ENGINE_SELECTION) { + GTEST_SKIP() << "This test requires Core ML 6 or later because it expects the factory to advertise an NPU."; + } + + EpFactoryInternal factory{std::make_unique()}; + OrtEpFactory* c_factory = &factory; + ASSERT_NE(c_factory->GetSupportedDevices, nullptr); + + const OrtHardwareDevice gpu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_GPU); + const OrtHardwareDevice npu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_NPU); + const OrtHardwareDevice second_gpu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_GPU); + const OrtHardwareDevice second_npu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_NPU); + const OrtHardwareDevice cpu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_CPU); + + constexpr size_t kOutputCapacity = 5; + OrtEpDevice untouched_marker{}; + + // Calls GetSupportedDevices on the given hardware devices with max_ep_devices as the output capacity. Returns the + // hardware device behind each published OrtEpDevice in publication order and releases the instances. Output + // entries beyond the published count must keep their marker value. + const auto published_hardware = [&](gsl::span hardware, size_t max_ep_devices) { + std::array ep_devices{}; + ep_devices.fill(&untouched_marker); + size_t num_ep_devices = 42; // a successful call must overwrite this + Ort::Status status{c_factory->GetSupportedDevices(c_factory, hardware.data(), hardware.size(), + ep_devices.data(), max_ep_devices, &num_ep_devices)}; + + InlinedVector result; + EXPECT_TRUE(status.IsOK()) << status.GetErrorMessage(); + EXPECT_LE(num_ep_devices, max_ep_devices); + if (!status.IsOK() || num_ep_devices > max_ep_devices) { + return result; + } + + for (size_t i = 0; i < ep_devices.size(); ++i) { + if (i >= num_ep_devices) { + EXPECT_EQ(ep_devices[i], &untouched_marker) << "Entry " << i << " lies beyond the published count."; + continue; + } + + EXPECT_NE(ep_devices[i], &untouched_marker) << "Published entry " << i << " was not written."; + EXPECT_NE(ep_devices[i], nullptr) << "Published entry " << i << " is null."; + if (ep_devices[i] == nullptr || ep_devices[i] == &untouched_marker) { + continue; + } + + result.push_back(Ort::GetApi().EpDevice_Device(ep_devices[i])); + Ort::GetEpApi().ReleaseEpDevice(ep_devices[i]); + } + + return result; + }; + + // Only the first GPU and NPU are advertised, in that order. + const std::array gpu_first{&gpu, &npu, &second_gpu, &second_npu, &cpu}; + EXPECT_THAT(published_hardware(gpu_first, kOutputCapacity), ::testing::ElementsAre(&gpu, &npu)); + + // Skip the leading CPU device and preserve the NPU-before-GPU order. + const std::array cpu_first{&cpu, &npu, &gpu}; + EXPECT_THAT(published_hardware(cpu_first, kOutputCapacity), ::testing::ElementsAre(&npu, &gpu)); + + // The output capacity limits the total number of published devices. A capacity of 1 keeps only the first + // supported device. A capacity of 0 produces no devices. + EXPECT_THAT(published_hardware(gpu_first, 1), ::testing::ElementsAre(&gpu)); + EXPECT_THAT(published_hardware(gpu_first, 0), ::testing::IsEmpty()); + + // When the input contains only a CPU device, no devices are published and the output count is set to zero. + const std::array cpu_only{&cpu}; + EXPECT_THAT(published_hardware(cpu_only, kOutputCapacity), ::testing::IsEmpty()); +} + +// If the second OrtEpDevice creation fails, GetSupportedDevices must return an error, release the first device, +// and leave the output array and count unchanged, following ORT's C API convention for failed calls. +// The test replaces the GetVersion callback on EpFactoryInternal with one that returns a valid version on the +// first call and an invalid version on the second. CreateEpDevice validates each version, causing the second +// creation to fail. +TEST(AutoEpSelection, CoreMLEPGetSupportedDevicesRollsBackOnCreateFailure) { + if (coreml::util::CoreMLVersion() < MINIMUM_COREML_VERSION_FOR_NEURAL_ENGINE_SELECTION) { + GTEST_SKIP() << "This test requires Core ML 6 or later so the factory can advertise both an NPU and a GPU."; + } + + EpFactoryInternal factory{std::make_unique()}; + OrtEpFactory* c_factory = &factory; + g_injected_version_calls = 0; + c_factory->GetVersion = [](const OrtEpFactory*) noexcept -> const char* { + // A valid version for the first device and an invalid one for the second, so the second CreateEpDevice fails. + return ++g_injected_version_calls == 1 ? "1.0.0" : "not a version"; + }; + + const OrtHardwareDevice npu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_NPU); + const OrtHardwareDevice gpu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_GPU); + const std::array hardware{&npu, &gpu}; + + OrtEpDevice untouched_marker{}; + std::array ep_devices{&untouched_marker, &untouched_marker}; + size_t num_ep_devices = 42; + Ort::Status status{c_factory->GetSupportedDevices(c_factory, hardware.data(), hardware.size(), ep_devices.data(), + ep_devices.size(), &num_ep_devices)}; + + ASSERT_FALSE(status.IsOK()) + << "GetSupportedDevices succeeded although the second CreateEpDevice was made to fail."; + EXPECT_EQ(g_injected_version_calls, 2) + << "GetVersion must be called for both device creation attempts."; + EXPECT_THAT(ep_devices, ::testing::Each(&untouched_marker)); + EXPECT_EQ(num_ep_devices, size_t{42}); +} + +#endif // defined(__APPLE__) + +#if !defined(__APPLE__) +// CoreML can be built on non-Apple platforms to test model conversion, but it cannot execute models there. +// The factory checks only the device type and Core ML version. Because the non-Apple CoreMLVersion stub reports +// Core ML 7, the factory would accept accelerators discovered on a non-Apple system. Verify that the platform guards +// prevent the CoreML factory from being registered on non-Apple builds. +// Registration is checked with GetHardwareDeviceEpIncompatibilityDetails rather than through the published device +// list. The function looks up the factory by EP name before checking hardware compatibility, so any discovered +// device, including the CPU, is sufficient and the test does not depend on accelerator discovery. +TEST(AutoEpSelection, CoreMLEPIsNotRegisteredOnNonApplePlatforms) { + const OrtApi* c_api = &Ort::GetApi(); + + size_t num_devices = 0; + ASSERT_ORTSTATUS_OK(c_api->GetNumHardwareDevices(*ort_env, &num_devices)); + ASSERT_GT(num_devices, 0u) << "Expected device discovery to report at least the CPU device."; + + InlinedVector devices(num_devices); + ASSERT_ORTSTATUS_OK(c_api->GetHardwareDevices(*ort_env, devices.data(), num_devices)); + + OrtDeviceEpIncompatibilityDetails* details = nullptr; + Ort::Status status{c_api->GetHardwareDeviceEpIncompatibilityDetails(*ort_env, kCoreMLExecutionProvider, + devices.front(), &details)}; + // If the CoreML factory is unexpectedly registered, the call succeeds and returns an object that must be released + // before the test reports the failure. + if (details != nullptr) { + c_api->ReleaseDeviceEpIncompatibilityDetails(details); + } + + ASSERT_FALSE(status.IsOK()) << "A factory named " << kCoreMLExecutionProvider << " is registered on a non-Apple " + << "platform, where CoreML cannot execute models."; + EXPECT_EQ(status.GetErrorCode(), ORT_INVALID_ARGUMENT); + // ORT_INVALID_ARGUMENT alone does not prove that the factory is absent. If the EP name matches a registered + // factory, GetHardwareDeviceEpIncompatibilityDetails invokes the factory's compatibility callback, + // which may return the same error code. Verify the diagnostic to confirm that no factory matched the CoreML EP name. + EXPECT_THAT(status.GetErrorMessage(), ::testing::HasSubstr("No valid factory found for execution provider")); +} +#endif // !defined(__APPLE__) + +} // namespace test +} // namespace onnxruntime + +#endif // defined(USE_COREML) From a061eaefbd60892c56009f833a0bcedd60bed520 Mon Sep 17 00:00:00 2001 From: Maxim Davgalev Date: Sat, 19 Sep 2026 11:23:09 -0700 Subject: [PATCH 09/12] Use pci_vendor_ids::kApple in the CoreML EP selection tests Apple device discovery now uses pci_vendor_ids::kApple. Update the tests to use the same constant. --- onnxruntime/test/autoep/test_selection_coreml.cc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/onnxruntime/test/autoep/test_selection_coreml.cc b/onnxruntime/test/autoep/test_selection_coreml.cc index abf49a09373c9..65b9fcd9414f0 100644 --- a/onnxruntime/test/autoep/test_selection_coreml.cc +++ b/onnxruntime/test/autoep/test_selection_coreml.cc @@ -21,6 +21,7 @@ #include "core/session/onnxruntime_cxx_api.h" #if defined(__APPLE__) +#include "core/common/pci_vendor_ids.h" #include "core/framework/ortdevice.h" #include "core/providers/coreml/coreml_provider_factory.h" #include "core/providers/coreml/model/host_utils.h" @@ -188,7 +189,7 @@ TEST(AutoEpSelection, CoreMLEP) { // Verify the CoreML factory's vendor name and vendor ID, along with the hardware vendor ID. The factory reports // "Microsoft", like the other internal factories: the factory vendor identifies who provides the EP // implementation, not the hardware vendor. - // Apple device discovery currently assigns Apple's PCI vendor id (0x106B) to every NPU and GPU it reports. The + // Apple device discovery assigns pci_vendor_ids::kApple to every NPU and GPU it reports. The // CoreML factory intentionally accepts any discovered GPU regardless of vendor, so this hardware vendor ID // assertion must be updated if discovery later reports a non-Apple GPU (see the multi-GPU TODO in // core/platform/apple/device_discovery.cc). @@ -204,7 +205,7 @@ TEST(AutoEpSelection, CoreMLEP) { } EXPECT_STREQ(c_api->EpDevice_EpVendor(ep_device), "Microsoft"); - EXPECT_EQ(c_api->HardwareDevice_VendorId(c_api->EpDevice_Device(ep_device)), uint32_t{0x106B}); + EXPECT_EQ(c_api->HardwareDevice_VendorId(c_api->EpDevice_Device(ep_device)), pci_vendor_ids::kApple); EXPECT_EQ(ep_device->ep_factory->GetVendorId(ep_device->ep_factory), OrtDevice::VendorIds::MICROSOFT); } @@ -493,7 +494,7 @@ namespace { OrtHardwareDevice MakeSyntheticHardwareDevice(OrtHardwareDeviceType type) { OrtHardwareDevice device{}; device.type = type; - device.vendor_id = 0x106B; // Apple's PCI vendor ID + device.vendor_id = pci_vendor_ids::kApple; device.vendor = "Apple"; return device; } From 662c4740b129e247164b5fbfb164ba5fc55307a1 Mon Sep 17 00:00:00 2001 From: Maxim Davgalev Date: Sat, 19 Sep 2026 11:23:09 -0700 Subject: [PATCH 10/12] Drop the redundant session checker from RunBasicTest Remove the redundant v2_session_checker parameter from RunBasicTest and the session_checker parameter from TestInference. --- onnxruntime/test/autoep/test_autoep_utils.h | 2 -- onnxruntime/test/autoep/test_selection.cc | 14 ++------------ onnxruntime/test/autoep/test_selection_coreml.cc | 14 ++++++-------- 3 files changed, 8 insertions(+), 22 deletions(-) diff --git a/onnxruntime/test/autoep/test_autoep_utils.h b/onnxruntime/test/autoep/test_autoep_utils.h index c67d98e81ac13..c57f68a1404cf 100644 --- a/onnxruntime/test/autoep/test_autoep_utils.h +++ b/onnxruntime/test/autoep/test_autoep_utils.h @@ -64,14 +64,12 @@ struct Utils { // ep_options on the V2 path. The V2 path uses the devices chosen by select_devices, or the first OrtEpDevice // advertised for ep_name when no selector is provided. If library_path is provided, the helper registers the // plugin EP library before creating the session. -// v2_session_checker runs after the V2 session is created and before inference. // disable_cpu_ep_fallback applies to both paths. It requires the selected EP to handle the entire graph, preventing // a false pass caused by unsupported nodes falling back to the ORT CPU EP. void RunBasicTest(const std::string& ep_name, std::optional library_path, const Ort::KeyValuePairs& provider_options = Ort::KeyValuePairs{}, const std::function&)>& select_devices = nullptr, bool test_auto_select = true, - const std::function& v2_session_checker = nullptr, bool disable_cpu_ep_fallback = false); } // namespace test diff --git a/onnxruntime/test/autoep/test_selection.cc b/onnxruntime/test/autoep/test_selection.cc index f9f88eae28b3d..8b84559019fbe 100644 --- a/onnxruntime/test/autoep/test_selection.cc +++ b/onnxruntime/test/autoep/test_selection.cc @@ -80,10 +80,7 @@ static void TestInference(Ort::Env& env, const std::basic_string& mod // EP. Session creation fails unless the selected non-CPU EP supports the // entire graph. Setting this to true while explicitly selecting the ORT // CPU EP is invalid and causes session creation to fail. - bool disable_cpu_ep_fallback = false, - // Optional callback invoked after session creation and before inference, - // for example to verify the session's EP assignment. - const std::function& session_checker = nullptr) { + bool disable_cpu_ep_fallback = false) { Ort::SessionOptions session_options; if (disable_cpu_ep_fallback) { @@ -138,11 +135,6 @@ static void TestInference(Ort::Env& env, const std::basic_string& mod // if session creation passes, model loads fine Ort::Session session(env, model_uri.c_str(), session_options); - if (session_checker) { - // Stop this helper if session_checker reports a fatal assertion, rather than continuing to RunSession. - ASSERT_NO_FATAL_FAILURE(session_checker(session)); - } - // caller wants to test running the model (not just loading the model) if (!test_session_creation_only) { auto default_allocator = std::make_unique(); @@ -160,7 +152,6 @@ void RunBasicTest(const std::string& ep_name, std::optional&)>& select_devices, bool test_auto_select, - const std::function& v2_session_checker, bool disable_cpu_ep_fallback) { const auto run_test = [&](bool auto_select) { std::vector> inputs(1); @@ -184,8 +175,7 @@ void RunBasicTest(const std::string& ep_name, std::optional= MINIMUM_COREML_VERSION; } -// Verifies that a session for mul_1.onnx assigns its input to the CoreML EP. Used as the session checker for -// RunBasicTest, which runs the inference itself afterwards. +// Verifies that a session for mul_1.onnx assigns its input to the CoreML EP. void AssertMul1InputAssignedToCoreML(Ort::Session& session) { const OrtApi* c_api = &Ort::GetApi(); const OrtEpDevice* input_ep_device = nullptr; @@ -211,10 +210,9 @@ TEST(AutoEpSelection, CoreMLEP) { // Both the "test.ep_to_select" auto-selection path and the AppendExecutionProvider_V2 path select CoreML as // the only non-CPU EP. Disabling CPU fallback makes session creation fail unless CoreML takes the entire graph. - // The V2 path also checks the input assignment directly. CoreMLEPPreferNpu and CoreMLEPPreferGpu perform the - // same assignment check for policy-based selection. + // CoreMLEPPreferNpu and CoreMLEPPreferGpu check the input assignment for policy-based selection. RunBasicTest(kCoreMLExecutionProvider, std::nullopt, Ort::KeyValuePairs{}, /*select_devices*/ nullptr, - /*test_auto_select*/ true, AssertMul1InputAssignedToCoreML, /*disable_cpu_ep_fallback*/ true); + /*test_auto_select*/ true, /*disable_cpu_ep_fallback*/ true); } // Tests explicit device selection through AppendExecutionProvider_V2. Each test case selects one or more CoreML @@ -286,12 +284,12 @@ TEST(AutoEpSelection, CoreMLEPExplicitDeviceSelection) { } // Run only the AppendExecutionProvider_V2 path because it passes the test case's full device selection to the - // factory. The "test.ep_to_select" path would pass only the first CoreML device. Verify the input assignment and - // disable ORT CPU fallback so CoreML must execute the entire graph. + // factory. The "test.ep_to_select" path would pass only the first CoreML device. Disable ORT CPU fallback so + // CoreML must execute the entire graph. RunBasicTest( kCoreMLExecutionProvider, std::nullopt, ep_options, [&devices](std::vector& selected) { selected = devices; }, - /*test_auto_select*/ false, AssertMul1InputAssignedToCoreML, /*disable_cpu_ep_fallback*/ true); + /*test_auto_select*/ false, /*disable_cpu_ep_fallback*/ true); } } From dadd730dac561b53f41154fc29a18353e1f47179 Mon Sep 17 00:00:00 2001 From: Maxim Davgalev Date: Sun, 20 Sep 2026 12:15:52 -0700 Subject: [PATCH 11/12] Report the CoreML EP's effective options via GetProviderOptions Add CoreMLOptions::ToProviderOptions() to report the effective options using the provider option names defined in coreml_provider_factory.h. --- .../coreml/coreml_execution_provider.cc | 4 ++ .../coreml/coreml_execution_provider.h | 2 + .../core/providers/coreml/coreml_options.cc | 30 ++++++++ .../core/providers/coreml/coreml_options.h | 5 ++ .../providers/coreml/coreml_basic_test.cc | 70 +++++++++++++++++++ 5 files changed, 111 insertions(+) diff --git a/onnxruntime/core/providers/coreml/coreml_execution_provider.cc b/onnxruntime/core/providers/coreml/coreml_execution_provider.cc index 9dd3dfaf6c75a..b78275df0a228 100644 --- a/onnxruntime/core/providers/coreml/coreml_execution_provider.cc +++ b/onnxruntime/core/providers/coreml/coreml_execution_provider.cc @@ -37,6 +37,10 @@ CoreMLExecutionProvider::CoreMLExecutionProvider(const CoreMLOptions& options) CoreMLExecutionProvider::~CoreMLExecutionProvider() {} +ProviderOptions CoreMLExecutionProvider::GetProviderOptions() const { + return coreml_options_.ToProviderOptions(); +} + std::vector> CoreMLExecutionProvider::GetCapability(const onnxruntime::GraphViewer& graph_viewer, const IKernelLookup& /*kernel_lookup*/, diff --git a/onnxruntime/core/providers/coreml/coreml_execution_provider.h b/onnxruntime/core/providers/coreml/coreml_execution_provider.h index 574ae1fc0106b..b91aeb2d8fd77 100644 --- a/onnxruntime/core/providers/coreml/coreml_execution_provider.h +++ b/onnxruntime/core/providers/coreml/coreml_execution_provider.h @@ -28,6 +28,8 @@ class CoreMLExecutionProvider : public IExecutionProvider { std::vector& node_compute_funcs) override; #endif + ProviderOptions GetProviderOptions() const override; + private: // The bit flags which define bool options for COREML EP, bits are defined as // COREMLFlags in include/onnxruntime/core/providers/coreml/coreml_provider_factory.h diff --git a/onnxruntime/core/providers/coreml/coreml_options.cc b/onnxruntime/core/providers/coreml/coreml_options.cc index daaab86be5046..7f89b7bd86e8a 100644 --- a/onnxruntime/core/providers/coreml/coreml_options.cc +++ b/onnxruntime/core/providers/coreml/coreml_options.cc @@ -98,4 +98,34 @@ void CoreMLOptions::ValidateAndParseProviderOption(const ProviderOptions& option } } } + +ProviderOptions CoreMLOptions::ToProviderOptions() const { + // MLComputeUnits uses constants defined in coreml_provider_factory.h. The other strings match + // those accepted by ValidateAndParseProviderOption. + const char* compute_units = kCoremlProviderOption_MLComputeUnits_ALL; + if (ComputeUnits(COREML_FLAG_USE_CPU_ONLY)) { + compute_units = kCoremlProviderOption_MLComputeUnits_CPUOnly; + } else if (ComputeUnits(COREML_FLAG_USE_CPU_AND_GPU)) { + compute_units = kCoremlProviderOption_MLComputeUnits_CPUAndGPU; + } else if (ComputeUnits(COREML_FLAG_ONLY_ENABLE_DEVICE_WITH_ANE)) { + compute_units = kCoremlProviderOption_MLComputeUnits_CPUAndNeuralEngine; + } + + ProviderOptions options{ + {kCoremlProviderOption_MLComputeUnits, compute_units}, + {kCoremlProviderOption_ModelFormat, create_mlprogram_ ? "MLProgram" : "NeuralNetwork"}, + {kCoremlProviderOption_RequireStaticInputShapes, require_static_shape_ ? "1" : "0"}, + {kCoremlProviderOption_EnableOnSubgraphs, enable_on_subgraph_ ? "1" : "0"}, + {kCoremlProviderOption_SpecializationStrategy, strategy_.empty() ? std::string{"Default"} : strategy_}, + // The effective value: profiling applies to MLProgram models only. + {kCoremlProviderOption_ProfileComputePlan, ProfileComputePlan() ? "1" : "0"}, + {kCoremlProviderOption_AllowLowPrecisionAccumulationOnGPU, allow_low_precision_accumulation_on_gpu_ ? "1" : "0"}, + }; + + if (!model_cache_directory_.empty()) { + options[kCoremlProviderOption_ModelCacheDirectory] = model_cache_directory_; + } + + return options; +} } // namespace onnxruntime diff --git a/onnxruntime/core/providers/coreml/coreml_options.h b/onnxruntime/core/providers/coreml/coreml_options.h index d7ee04b3f8a79..fe4f65a9ab13c 100644 --- a/onnxruntime/core/providers/coreml/coreml_options.h +++ b/onnxruntime/core/providers/coreml/coreml_options.h @@ -5,6 +5,7 @@ #include "core/common/inlined_containers.h" #include "core/framework/execution_provider.h" +#include "core/framework/provider_options.h" namespace onnxruntime { @@ -36,6 +37,10 @@ class CoreMLOptions { bool ProfileComputePlan() const { return profile_compute_plan_ && create_mlprogram_; } std::string_view ModelCacheDirectory() const { return model_cache_directory_; } + + // Returns the effective options for IExecutionProvider::GetProviderOptions using the provider option names + // defined in coreml_provider_factory.h. Options supplied through legacy flags are reported in the same format. + ProviderOptions ToProviderOptions() const; // The options specified by the user are const, but if there's an error setting up caching we disable it // so that the EP can still be used. The error is logged for the user to investigate. void DisableModelCache() const { model_cache_directory_.clear(); } diff --git a/onnxruntime/test/providers/coreml/coreml_basic_test.cc b/onnxruntime/test/providers/coreml/coreml_basic_test.cc index 2fb8d1a09d873..81fd46dee2365 100644 --- a/onnxruntime/test/providers/coreml/coreml_basic_test.cc +++ b/onnxruntime/test/providers/coreml/coreml_basic_test.cc @@ -3736,5 +3736,75 @@ TEST(CoreMLExecutionProviderTest, GatherScalarIndicesRank5DataNotSupported) { } #endif // !(ORT_MINIMAL_BUILD) + +// Verifies that GetProviderOptions reports effective CoreML options using public option names, +// whether the EP is configured through ProviderOptions or legacy flags. +TEST(CoreMLExecutionProviderTest, GetProviderOptionsReportsEffectiveOptions) { + const auto reported_options = [](const ProviderOptions& options) { + return CoreMLProviderFactoryCreator::Create(options)->CreateProvider()->GetProviderOptions(); + }; + + // Default values are reported even when no options are provided. An unset cache directory is omitted. + { + const ProviderOptions reported = reported_options({}); + EXPECT_EQ(reported.at(kCoremlProviderOption_MLComputeUnits), kCoremlProviderOption_MLComputeUnits_ALL); + EXPECT_EQ(reported.at(kCoremlProviderOption_ModelFormat), "NeuralNetwork"); + EXPECT_EQ(reported.at(kCoremlProviderOption_RequireStaticInputShapes), "0"); + EXPECT_EQ(reported.at(kCoremlProviderOption_EnableOnSubgraphs), "0"); + EXPECT_EQ(reported.at(kCoremlProviderOption_SpecializationStrategy), "Default"); + EXPECT_EQ(reported.at(kCoremlProviderOption_ProfileComputePlan), "0"); + EXPECT_EQ(reported.at(kCoremlProviderOption_AllowLowPrecisionAccumulationOnGPU), "0"); + EXPECT_EQ(reported.count(kCoremlProviderOption_ModelCacheDirectory), size_t{0}); + } + + // Explicitly provided option values are reported unchanged. + { + const std::string cache_directory = + std::filesystem::temp_directory_path().append("coreml_provider_options_test").string(); + const ProviderOptions reported = reported_options({ + {kCoremlProviderOption_MLComputeUnits, kCoremlProviderOption_MLComputeUnits_CPUOnly}, + {kCoremlProviderOption_ModelFormat, "MLProgram"}, + {kCoremlProviderOption_RequireStaticInputShapes, "1"}, + {kCoremlProviderOption_EnableOnSubgraphs, "1"}, + {kCoremlProviderOption_SpecializationStrategy, "FastPrediction"}, + {kCoremlProviderOption_ProfileComputePlan, "1"}, + {kCoremlProviderOption_AllowLowPrecisionAccumulationOnGPU, "1"}, + {kCoremlProviderOption_ModelCacheDirectory, cache_directory}, + }); + EXPECT_EQ(reported.at(kCoremlProviderOption_MLComputeUnits), kCoremlProviderOption_MLComputeUnits_CPUOnly); + EXPECT_EQ(reported.at(kCoremlProviderOption_ModelFormat), "MLProgram"); + EXPECT_EQ(reported.at(kCoremlProviderOption_RequireStaticInputShapes), "1"); + EXPECT_EQ(reported.at(kCoremlProviderOption_EnableOnSubgraphs), "1"); + EXPECT_EQ(reported.at(kCoremlProviderOption_SpecializationStrategy), "FastPrediction"); + EXPECT_EQ(reported.at(kCoremlProviderOption_ProfileComputePlan), "1"); + EXPECT_EQ(reported.at(kCoremlProviderOption_AllowLowPrecisionAccumulationOnGPU), "1"); + EXPECT_EQ(reported.at(kCoremlProviderOption_ModelCacheDirectory), cache_directory); + } + + // ProfileComputePlan applies only to MLProgram, so it is reported as "0" for NeuralNetwork even when set to "1". + EXPECT_EQ(reported_options({{kCoremlProviderOption_ProfileComputePlan, "1"}}) + .at(kCoremlProviderOption_ProfileComputePlan), + "0"); + + EXPECT_EQ(reported_options({{kCoremlProviderOption_MLComputeUnits, kCoremlProviderOption_MLComputeUnits_CPUAndGPU}}) + .at(kCoremlProviderOption_MLComputeUnits), + kCoremlProviderOption_MLComputeUnits_CPUAndGPU); + + EXPECT_EQ(reported_options({{kCoremlProviderOption_MLComputeUnits, + kCoremlProviderOption_MLComputeUnits_CPUAndNeuralEngine}}) + .at(kCoremlProviderOption_MLComputeUnits), + kCoremlProviderOption_MLComputeUnits_CPUAndNeuralEngine); + + // The legacy flags constructor reports the same names and values. + { + const ProviderOptions reported = + CoreMLProviderFactoryCreator::Create(COREML_FLAG_USE_CPU_ONLY | COREML_FLAG_CREATE_MLPROGRAM) + ->CreateProvider() + ->GetProviderOptions(); + EXPECT_EQ(reported.at(kCoremlProviderOption_MLComputeUnits), kCoremlProviderOption_MLComputeUnits_CPUOnly); + EXPECT_EQ(reported.at(kCoremlProviderOption_ModelFormat), "MLProgram"); + EXPECT_EQ(reported.count(kCoremlProviderOption_ModelCacheDirectory), size_t{0}); + } +} } // namespace test } // namespace onnxruntime From 0917c1cafcaf5c6b84cc31a68614f30655c20ffa Mon Sep 17 00:00:00 2001 From: Maxim Davgalev Date: Sun, 20 Sep 2026 12:15:52 -0700 Subject: [PATCH 12/12] Test CoreML compute units for device selection Verify that the factory derives MLComputeUnits from the selected devices and preserves a compatible caller-provided value. --- .../test/autoep/test_selection_coreml.cc | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/onnxruntime/test/autoep/test_selection_coreml.cc b/onnxruntime/test/autoep/test_selection_coreml.cc index 6753d815d5f55..77c81a3bee5f5 100644 --- a/onnxruntime/test/autoep/test_selection_coreml.cc +++ b/onnxruntime/test/autoep/test_selection_coreml.cc @@ -21,7 +21,9 @@ #include "core/session/onnxruntime_cxx_api.h" #if defined(__APPLE__) +#include "core/common/logging/logging.h" #include "core/common/pci_vendor_ids.h" +#include "core/framework/execution_provider.h" #include "core/framework/ortdevice.h" #include "core/providers/coreml/coreml_provider_factory.h" #include "core/providers/coreml/model/host_utils.h" @@ -35,6 +37,7 @@ #include "test/autoep/test_autoep_utils.h" #include "test/shared_lib/utils.h" #include "test/util/include/api_asserts.h" +#include "test/util/include/asserts.h" extern std::unique_ptr ort_env; @@ -481,7 +484,7 @@ TEST(AutoEpSelection, CoreMLEPUnknownComputeUnitsRejectedByProvider) { } } -// The two tests below call CoreMLEpFactory through its EpFactoryInternal wrapper using synthetic hardware devices. +// The tests below call CoreMLEpFactory through its EpFactoryInternal wrapper using synthetic hardware devices. // Device advertising depends only on the device type and runtime Core ML version, so the tests require no physical // accelerator and can run on Intel Macs that meet the version requirement. Any published OrtEpDevice instances are // released through the EP API. @@ -497,6 +500,28 @@ OrtHardwareDevice MakeSyntheticHardwareDevice(OrtHardwareDeviceType type) { return device; } +// Creates the CoreML EP for the given devices through CoreMLEpFactory::CreateIExecutionProvider and writes +// its reported MLComputeUnits value to compute_units. If requested_compute_units is non-null, it is passed through +// the session options so the factory validates it against the selection instead of deriving a default. +void GetMLComputeUnitsForSelection(gsl::span devices, + const char* requested_compute_units, std::string& compute_units) { + EpFactoryInternal factory{std::make_unique()}; + + OrtSessionOptions session_options{}; + if (requested_compute_units != nullptr) { + const std::string key = OrtSessionOptions::GetProviderOptionPrefix(kCoreMLExecutionProvider) + + kCoremlProviderOption_MLComputeUnits; + ASSERT_STATUS_OK(session_options.value.config_options.AddConfigEntry(key.c_str(), requested_compute_units)); + } + + std::unique_ptr ep; + ASSERT_ORTSTATUS_OK(factory.CreateIExecutionProvider(devices.data(), nullptr, devices.size(), &session_options, + logging::LoggingManager::DefaultLogger().ToExternal(), &ep)); + ASSERT_NE(ep, nullptr); + + compute_units = ep->GetProviderOptions().at(kCoremlProviderOption_MLComputeUnits); +} + // Number of calls to the injected GetVersion callback. int g_injected_version_calls = 0; @@ -614,6 +639,40 @@ TEST(AutoEpSelection, CoreMLEPGetSupportedDevicesRollsBackOnCreateFailure) { EXPECT_EQ(num_ep_devices, size_t{42}); } +// Verifies that CreateIExecutionProvider derives MLComputeUnits from the selected devices and preserves a +// caller-provided value compatible with the selection. Synthetic devices let these cases run without physical +// accelerators. +TEST(AutoEpSelection, CoreMLEPDerivedMLComputeUnits) { + const OrtHardwareDevice npu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_NPU); + const OrtHardwareDevice gpu = MakeSyntheticHardwareDevice(OrtHardwareDeviceType_GPU); + const std::array npu_only{&npu}; + const std::array gpu_only{&gpu}; + const std::array npu_and_gpu{&npu, &gpu}; + std::string compute_units; + + ASSERT_NO_FATAL_FAILURE(GetMLComputeUnitsForSelection(npu_only, nullptr, compute_units)); + EXPECT_EQ(compute_units, kCoremlProviderOption_MLComputeUnits_CPUAndNeuralEngine); + + ASSERT_NO_FATAL_FAILURE(GetMLComputeUnitsForSelection(gpu_only, nullptr, compute_units)); + EXPECT_EQ(compute_units, kCoremlProviderOption_MLComputeUnits_CPUAndGPU); + + ASSERT_NO_FATAL_FAILURE(GetMLComputeUnitsForSelection(npu_and_gpu, nullptr, compute_units)); + EXPECT_EQ(compute_units, kCoremlProviderOption_MLComputeUnits_ALL); + + // The caller can disable some or all selected accelerators. The factory keeps the provided MLComputeUnits value. + ASSERT_NO_FATAL_FAILURE( + GetMLComputeUnitsForSelection(gpu_only, kCoremlProviderOption_MLComputeUnits_CPUOnly, compute_units)); + EXPECT_EQ(compute_units, kCoremlProviderOption_MLComputeUnits_CPUOnly); + + ASSERT_NO_FATAL_FAILURE( + GetMLComputeUnitsForSelection(npu_and_gpu, kCoremlProviderOption_MLComputeUnits_CPUAndGPU, compute_units)); + EXPECT_EQ(compute_units, kCoremlProviderOption_MLComputeUnits_CPUAndGPU); + + ASSERT_NO_FATAL_FAILURE(GetMLComputeUnitsForSelection( + npu_and_gpu, kCoremlProviderOption_MLComputeUnits_CPUAndNeuralEngine, compute_units)); + EXPECT_EQ(compute_units, kCoremlProviderOption_MLComputeUnits_CPUAndNeuralEngine); +} + #endif // defined(__APPLE__) #if !defined(__APPLE__)