From 10346edc5cdef102041d8575e7c10e03ca53968f Mon Sep 17 00:00:00 2001 From: David Fan Date: Fri, 22 May 2026 13:04:01 -0700 Subject: [PATCH 1/4] Fix: Skip CPU provider in SetProviderSessionOptions CPU EP is always implicitly registered in ORT sessions. Passing 'cpu' to ORT's SessionOptionsAppendExecutionProvider causes a RuntimeError in ORT 1.24+ which introduced a strict provider name whitelist that does not include CPU (#24433). Previously this worked because older ORT versions silently ignored unknown provider names. With ORT 1.24+, explicitly appending 'cpu' now fails with 'Unknown provider name'. The fix skips CPU/cpu/CPUExecutionProvider in the provider iteration loop since it never needs to be explicitly appended. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/models/session_options.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/models/session_options.cpp b/src/models/session_options.cpp index 4127b07108..9dd6215a5b 100644 --- a/src/models/session_options.cpp +++ b/src/models/session_options.cpp @@ -173,6 +173,12 @@ DeviceInterface* SetProviderSessionOptions(OrtSessionOptions& session_options, } for (const auto& provider : providers_list) { + // CPU EP is always implicitly registered, skip it to avoid passing it + // to ORT's AppendExecutionProvider which does not accept "cpu" as a name. + if (provider == "cpu" || provider == "CPU" || provider == "CPUExecutionProvider") { + continue; + } + auto provider_options_it = std::find_if(provider_options_list.begin(), provider_options_list.end(), [&provider](const Config::ProviderOptions& po) { return po.name == provider; }); From 6f44a18933fca20358de6e2ca2cc91081b69510b Mon Sep 17 00:00:00 2001 From: David Fan Date: Fri, 22 May 2026 13:15:46 -0700 Subject: [PATCH 2/4] Address review: case-insensitive CPU check and regression test - Use case-insensitive comparison for CPU provider name detection (handles 'cpu', 'CPU', 'Cpu', 'cpuexecutionprovider', etc.) - Add regression test in c_api_tests.cpp that verifies appending CPU provider variants does not throw Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/models/session_options.cpp | 11 +++++++++-- test/c_api_tests.cpp | 25 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/models/session_options.cpp b/src/models/session_options.cpp index 9dd6215a5b..37447aaad6 100644 --- a/src/models/session_options.cpp +++ b/src/models/session_options.cpp @@ -3,6 +3,8 @@ #include "session_options.h" +#include +#include #include #include @@ -175,8 +177,13 @@ DeviceInterface* SetProviderSessionOptions(OrtSessionOptions& session_options, for (const auto& provider : providers_list) { // CPU EP is always implicitly registered, skip it to avoid passing it // to ORT's AppendExecutionProvider which does not accept "cpu" as a name. - if (provider == "cpu" || provider == "CPU" || provider == "CPUExecutionProvider") { - continue; + { + std::string lower_provider(provider); + std::transform(lower_provider.begin(), lower_provider.end(), lower_provider.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (lower_provider == "cpu" || lower_provider == "cpuexecutionprovider") { + continue; + } } auto provider_options_it = std::find_if(provider_options_list.begin(), provider_options_list.end(), diff --git a/test/c_api_tests.cpp b/test/c_api_tests.cpp index 925f06432b..136c3afffd 100644 --- a/test/c_api_tests.cpp +++ b/test/c_api_tests.cpp @@ -48,6 +48,31 @@ TEST(CAPITests, Config) { #endif } +// Regression test: appending CPU provider should not throw. +// See https://github.com/microsoft/onnxruntime-genai/pull/2179 +TEST(CAPITests, AppendCpuProvider) { +#if TEST_PHI2 + auto config = OgaConfig::Create(PHI2_PATH); + config->ClearProviders(); + config->AppendProvider("cpu"); + auto model = OgaModel::Create(*config); + ASSERT_NE(model.get(), nullptr); + + // Also test other case variants + auto config2 = OgaConfig::Create(PHI2_PATH); + config2->ClearProviders(); + config2->AppendProvider("CPU"); + auto model2 = OgaModel::Create(*config2); + ASSERT_NE(model2.get(), nullptr); + + auto config3 = OgaConfig::Create(PHI2_PATH); + config3->ClearProviders(); + config3->AppendProvider("CPUExecutionProvider"); + auto model3 = OgaModel::Create(*config3); + ASSERT_NE(model3.get(), nullptr); +#endif +} + TEST(CAPITests, TokenizerCAPI) { #if TEST_PHI2 auto config = OgaConfig::Create(PHI2_PATH); From bf992a4b1bf591f5b6fe0d03d4daa57bcb9e38c0 Mon Sep 17 00:00:00 2001 From: David Fan Date: Fri, 22 May 2026 13:28:03 -0700 Subject: [PATCH 3/4] Throw if CPU provider options are set, skip bare CPU as no-op Address reviewer concern: instead of silently skipping CPU with provider options (which would be lost), throw a clear error message explaining that CPU EP doesn't support provider options and doesn't need explicit registration. Bare 'cpu' with no options is still accepted as a no-op for backward compatibility. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/models/session_options.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/models/session_options.cpp b/src/models/session_options.cpp index 37447aaad6..4461245658 100644 --- a/src/models/session_options.cpp +++ b/src/models/session_options.cpp @@ -175,13 +175,23 @@ DeviceInterface* SetProviderSessionOptions(OrtSessionOptions& session_options, } for (const auto& provider : providers_list) { - // CPU EP is always implicitly registered, skip it to avoid passing it - // to ORT's AppendExecutionProvider which does not accept "cpu" as a name. + // CPU EP is always implicitly registered and cannot be appended via + // ORT's AppendExecutionProvider API. Throw if users attempt to set + // CPU-specific provider options (which would be silently lost), but + // accept bare "cpu" as a no-op for backward compatibility. { std::string lower_provider(provider); std::transform(lower_provider.begin(), lower_provider.end(), lower_provider.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (lower_provider == "cpu" || lower_provider == "cpuexecutionprovider") { + auto provider_options_it = std::find_if(provider_options_list.begin(), provider_options_list.end(), + [&provider](const Config::ProviderOptions& po) { return po.name == provider; }); + if (provider_options_it != provider_options_list.end() && !provider_options_it->options.empty()) { + throw std::runtime_error( + "CPU execution provider does not support provider options. " + "CPU is always available as the default fallback and does not need to be explicitly registered. " + "Remove the CPU provider entry and its options from your configuration."); + } continue; } } From 160216e201ffbfbab203c4f5ababe11ba655e4ff Mon Sep 17 00:00:00 2001 From: David Fan Date: Fri, 22 May 2026 16:29:24 -0700 Subject: [PATCH 4/4] Refactor: register CPU in dispatch table and NormalizeProviderName Per reviewer feedback, instead of special-case inline logic: - Add 'cpu'/'cpuexecutionprovider' -> 'CPU' mapping to NormalizeProviderName - Register CPUAppendExecutionProvider as a no-op lambda in the dispatch table - Remove inline case-insensitive skip block and unnecessary include This follows the existing provider registration pattern. The lambda still throws if provider options are set (CPU EP doesn't support them). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/config.cpp | 4 +++- src/models/session_options.cpp | 38 ++++++++++++++-------------------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index d282efdb95..1e8a727420 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -18,7 +18,9 @@ namespace Generators { std::string_view NormalizeProviderName(std::string_view name) { std::string lower_name(name); std::transform(lower_name.begin(), lower_name.end(), lower_name.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); - if (lower_name == "qnn") { + if (lower_name == "cpu" || lower_name == "cpuexecutionprovider") { + return "CPU"; + } else if (lower_name == "qnn") { return "QNN"; } else if (lower_name == "webgpu") { return "WebGPU"; diff --git a/src/models/session_options.cpp b/src/models/session_options.cpp index 4461245658..db3efb98dd 100644 --- a/src/models/session_options.cpp +++ b/src/models/session_options.cpp @@ -4,7 +4,6 @@ #include "session_options.h" #include -#include #include #include @@ -147,9 +146,25 @@ DeviceInterface* SetProviderSessionOptions(OrtSessionOptions& session_options, const Config&, bool); + // CPU EP is always implicitly registered — no action needed. + // Throws if users attempt to set provider options (which would be silently lost). + static auto CPUAppendExecutionProvider = [](OrtSessionOptions&, + const Config::ProviderOptions& provider_options, + const Config&, + bool) -> DeviceInterface* { + if (!provider_options.options.empty()) { + throw std::runtime_error( + "CPU execution provider does not support provider options. " + "CPU is always available as the default fallback and does not need to be explicitly registered. " + "Remove the CPU provider entry and its options from your configuration."); + } + return nullptr; + }; + // Dispatch table: maps provider name (as it appears in genai_config.json) to // the corresponding provider-specific AppendExecutionProvider function. static const std::unordered_map append_execution_provider{ + {"CPU", CPUAppendExecutionProvider}, {"cuda", CUDAExecutionProvider::AppendExecutionProvider}, {"DML", DMLExecutionProvider::AppendExecutionProvider}, {"NvTensorRtRtx", NvTensorRtRtxExecutionProvider::AppendExecutionProvider}, @@ -175,27 +190,6 @@ DeviceInterface* SetProviderSessionOptions(OrtSessionOptions& session_options, } for (const auto& provider : providers_list) { - // CPU EP is always implicitly registered and cannot be appended via - // ORT's AppendExecutionProvider API. Throw if users attempt to set - // CPU-specific provider options (which would be silently lost), but - // accept bare "cpu" as a no-op for backward compatibility. - { - std::string lower_provider(provider); - std::transform(lower_provider.begin(), lower_provider.end(), lower_provider.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - if (lower_provider == "cpu" || lower_provider == "cpuexecutionprovider") { - auto provider_options_it = std::find_if(provider_options_list.begin(), provider_options_list.end(), - [&provider](const Config::ProviderOptions& po) { return po.name == provider; }); - if (provider_options_it != provider_options_list.end() && !provider_options_it->options.empty()) { - throw std::runtime_error( - "CPU execution provider does not support provider options. " - "CPU is always available as the default fallback and does not need to be explicitly registered. " - "Remove the CPU provider entry and its options from your configuration."); - } - continue; - } - } - auto provider_options_it = std::find_if(provider_options_list.begin(), provider_options_list.end(), [&provider](const Config::ProviderOptions& po) { return po.name == provider; });