From 36ca1d63e0c9b3f19bcd7487160fd4b6432bd446 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Thu, 14 May 2026 15:36:57 -0700 Subject: [PATCH 01/48] Add EPContext data I/O callbacks --- .../core/session/onnxruntime_c_api.h | 74 +++++++++ .../core/session/onnxruntime_cxx_api.h | 5 + .../core/session/onnxruntime_cxx_inline.h | 13 ++ .../core/session/onnxruntime_ep_c_api.h | 72 +++++++++ .../core/framework/ep_context_options.cc | 4 + .../core/framework/ep_context_options.h | 11 ++ onnxruntime/core/framework/session_options.h | 3 + .../core/session/abi_session_options.cc | 12 ++ onnxruntime/core/session/compile_api.cc | 24 +++ onnxruntime/core/session/compile_api.h | 3 + .../core/session/model_compilation_options.cc | 7 + .../core/session/model_compilation_options.h | 7 + onnxruntime/core/session/onnxruntime_c_api.cc | 1 + onnxruntime/core/session/ort_apis.h | 2 + onnxruntime/core/session/plugin_ep/ep_api.cc | 145 ++++++++++++++++++ onnxruntime/core/session/plugin_ep/ep_api.h | 19 +++ .../test/framework/ep_plugin_provider_test.cc | 132 ++++++++++++++++ 17 files changed, 534 insertions(+) diff --git a/include/onnxruntime/core/session/onnxruntime_c_api.h b/include/onnxruntime/core/session/onnxruntime_c_api.h index 0289bca5c84d2..696bd57ab797a 100644 --- a/include/onnxruntime/core/session/onnxruntime_c_api.h +++ b/include/onnxruntime/core/session/onnxruntime_c_api.h @@ -603,6 +603,47 @@ typedef OrtStatus*(ORT_API_CALL* OrtWriteBufferFunc)(_In_ void* state, _In_ const void* buffer, _In_ size_t buffer_num_bytes); +/** \brief Function called to write EPContext binary data during compilation. + * + * This function may be called repeatedly with chunks until the entire EPContext binary for a given file_name has been + * written. The application's implementation can process the data in any way (e.g., encrypt and store, upload to cloud + * storage, or compress) before persisting it. + * + * \param[in] state Opaque pointer holding the user's state. + * \param[in] file_name The intended EPContext binary file name as a null-terminated UTF-8 string. + * \param[in] buffer The buffer containing EPContext binary data to write. + * \param[in] buffer_num_bytes The size of the buffer in bytes. + * + * \return OrtStatus* Write status. Return nullptr on success. + * Use CreateStatus to provide error info with ORT_FAIL as the error code. + * ORT will release the OrtStatus* if not null. + */ +typedef OrtStatus*(ORT_API_CALL* OrtWriteEpContextDataFunc)(_In_ void* state, + _In_ const char* file_name, + _In_ const void* buffer, + _In_ size_t buffer_num_bytes); + +/** \brief Function called by ORT to read EPContext binary data during session load. + * + * The application reads, processes (e.g., decrypts, decompresses, downloads), and returns the EPContext binary data. + * ORT provides an allocator so the application can allocate the output buffer directly. + * + * \param[in] state Opaque pointer holding the user's state. + * \param[in] file_name The EPContext binary file name as a null-terminated UTF-8 string. + * \param[in] allocator ORT-provided allocator. The application must use this to allocate the output buffer. + * \param[out] buffer Set by the implementation to the allocated buffer containing the output data. + * \param[out] data_size Set by the implementation to the size of the output data in bytes. + * + * \return OrtStatus* Read status. Return nullptr on success. + * Use CreateStatus to provide error info with ORT_FAIL as the error code. + * ORT will release the OrtStatus* if not null. + */ +typedef OrtStatus*(ORT_API_CALL* OrtReadEpContextDataFunc)(_In_ void* state, + _In_ const char* file_name, + _In_ OrtAllocator* allocator, + _Outptr_ void** buffer, + _Out_ size_t* data_size); + /** \brief Function called by ORT to allow user to specify how an initializer should be saved, that is, either * written to an external file or stored within the model. ORT calls this function for every initializer when * generating a model. @@ -7511,6 +7552,22 @@ struct OrtApi { * \since Version 1.28. */ ORT_API_T(OrtExperimentalFnPtr, GetExperimentalFunction, _In_ const char* name); + + /** \brief Registers a callback to provide EPContext binary data during session load. + * + * When loading a compiled model with external (non-embedded) EPContext binary data, an execution provider can use + * OrtEpApi::ReadEpContextData to call this callback instead of reading the binary data from disk. + * + * \param[in] options The OrtSessionOptions instance. + * \param[in] read_func The OrtReadEpContextDataFunc callback. + * \param[in] state Opaque state passed to read_func. Can be NULL. + * + * \snippet{doc} snippets.dox OrtStatus Return Value + * + * \since Version 1.27. + */ + ORT_API2_STATUS(SessionOptions_SetEpContextDataReadFunc, _Inout_ OrtSessionOptions* options, + _In_ OrtReadEpContextDataFunc read_func, _In_opt_ void* state); }; /* @@ -8353,6 +8410,23 @@ struct OrtCompileApi { ORT_API2_STATUS(ModelCompilationOptions_SetInputModel, _In_ OrtModelCompilationOptions* model_compile_options, _In_ const OrtModel* model); + + /** \brief Sets a callback for writing EPContext binary data during compilation. + * + * When EPContext embed mode is disabled, execution providers can use OrtEpApi::WriteEpContextData to call this + * callback instead of writing EPContext binary data directly to disk. + * + * \param[in] model_compile_options The OrtModelCompilationOptions instance. + * \param[in] write_func The OrtWriteEpContextDataFunc called to stream out EPContext bytes. + * \param[in] state Opaque state passed to write_func. Can be NULL. + * + * \snippet{doc} snippets.dox OrtStatus Return Value + * + * \since Version 1.27. + */ + ORT_API2_STATUS(ModelCompilationOptions_SetEpContextDataWriteFunc, + _In_ OrtModelCompilationOptions* model_compile_options, + _In_ OrtWriteEpContextDataFunc write_func, _In_opt_ void* state); }; /** diff --git a/include/onnxruntime/core/session/onnxruntime_cxx_api.h b/include/onnxruntime/core/session/onnxruntime_cxx_api.h index 4798d3d4ad1b8..3b5d38593bc3f 100644 --- a/include/onnxruntime/core/session/onnxruntime_cxx_api.h +++ b/include/onnxruntime/core/session/onnxruntime_cxx_api.h @@ -1668,6 +1668,8 @@ struct SessionOptionsImpl : ConstSessionOptionsImpl { const std::vector& external_initializer_file_buffer_array, const std::vector& external_initializer_file_lengths); ///< Wraps OrtApi::AddExternalInitializersFromFilesInMemory + SessionOptionsImpl& SetEpContextDataReadFunc(OrtReadEpContextDataFunc read_func, void* state); ///< Wraps OrtApi::SessionOptions_SetEpContextDataReadFunc + SessionOptionsImpl& AppendExecutionProvider_CPU(int use_arena); ///< Wraps OrtApi::SessionOptionsAppendExecutionProvider_CPU SessionOptionsImpl& AppendExecutionProvider_CUDA(const OrtCUDAProviderOptions& provider_options); ///< Wraps OrtApi::SessionOptionsAppendExecutionProvider_CUDA SessionOptionsImpl& AppendExecutionProvider_CUDA_V2(const OrtCUDAProviderOptionsV2& provider_options); ///< Wraps OrtApi::SessionOptionsAppendExecutionProvider_CUDA_V2 @@ -1769,6 +1771,9 @@ struct ModelCompilationOptions : detail::Base { ///< Wraps OrtApi::ModelCompilationOptions_SetOutputModelWriteFunc ModelCompilationOptions& SetOutputModelWriteFunc(OrtWriteBufferFunc write_func, void* state); + ///< Wraps OrtCompileApi::ModelCompilationOptions_SetEpContextDataWriteFunc + ModelCompilationOptions& SetEpContextDataWriteFunc(OrtWriteEpContextDataFunc write_func, void* state); + ModelCompilationOptions& SetEpContextBinaryInformation(const ORTCHAR_T* output_directory, const ORTCHAR_T* model_name); ///< Wraps OrtApi::ModelCompilationOptions_SetEpContextBinaryInformation ModelCompilationOptions& SetFlags(uint32_t flags); ///< Wraps OrtApi::ModelCompilationOptions_SetFlags diff --git a/include/onnxruntime/core/session/onnxruntime_cxx_inline.h b/include/onnxruntime/core/session/onnxruntime_cxx_inline.h index d7439e7b356c6..303de4f635461 100644 --- a/include/onnxruntime/core/session/onnxruntime_cxx_inline.h +++ b/include/onnxruntime/core/session/onnxruntime_cxx_inline.h @@ -1335,6 +1335,12 @@ inline ModelCompilationOptions& ModelCompilationOptions::SetOutputModelWriteFunc return *this; } +inline ModelCompilationOptions& ModelCompilationOptions::SetEpContextDataWriteFunc( + OrtWriteEpContextDataFunc write_func, void* state) { + Ort::ThrowOnError(GetCompileApi().ModelCompilationOptions_SetEpContextDataWriteFunc(this->p_, write_func, state)); + return *this; +} + inline ModelCompilationOptions& ModelCompilationOptions::SetEpContextEmbedMode( bool embed_ep_context_in_model) { Ort::ThrowOnError(GetCompileApi().ModelCompilationOptions_SetEpContextEmbedMode( @@ -1574,6 +1580,13 @@ inline SessionOptionsImpl& SessionOptionsImpl::AddExternalInitializersFrom return *this; } +template +inline SessionOptionsImpl& SessionOptionsImpl::SetEpContextDataReadFunc(OrtReadEpContextDataFunc read_func, + void* state) { + ThrowOnError(GetApi().SessionOptions_SetEpContextDataReadFunc(this->p_, read_func, state)); + return *this; +} + template inline SessionOptionsImpl& SessionOptionsImpl::AppendExecutionProvider_CPU(int use_arena) { ThrowOnError(OrtSessionOptionsAppendExecutionProvider_CPU(this->p_, use_arena)); diff --git a/include/onnxruntime/core/session/onnxruntime_ep_c_api.h b/include/onnxruntime/core/session/onnxruntime_ep_c_api.h index 6824b85ba3001..4f439c90cdded 100644 --- a/include/onnxruntime/core/session/onnxruntime_ep_c_api.h +++ b/include/onnxruntime/core/session/onnxruntime_ep_c_api.h @@ -18,6 +18,7 @@ extern "C" { * @{ */ ORT_RUNTIME_CLASS(Ep); +ORT_RUNTIME_CLASS(EpContextConfig); ORT_RUNTIME_CLASS(EpFactory); ORT_RUNTIME_CLASS(EpGraphSupportInfo); ORT_RUNTIME_CLASS(MemoryDevice); // opaque class to wrap onnxruntime::OrtDevice @@ -2077,6 +2078,77 @@ struct OrtEpApi { ORT_API2_STATUS(ProfilingEventsContainer_AddEvents, _In_ OrtProfilingEventsContainer* events_container, _In_reads_(num_events) const OrtProfilingEvent* const* events, _In_ size_t num_events); + + /** \brief Get the EPContext configuration from session options. + * + * Extracts EPContext-related data I/O callbacks from the session options into an opaque OrtEpContextConfig handle. + * The EP should call this during CreateEp() while session_options is still valid, and store the returned handle for + * use during Compile(). The returned config is always non-NULL and must be released with ReleaseEpContextConfig. + * + * \param[in] session_options The OrtSessionOptions instance. + * \param[out] config The extracted OrtEpContextConfig. + * + * \snippet{doc} snippets.dox OrtStatus Return Value + * + * \since Version 1.27. + */ + ORT_API2_STATUS(SessionOptions_GetEpContextConfig, + _In_ const OrtSessionOptions* session_options, + _Outptr_ OrtEpContextConfig** config); + + /** \brief Release an OrtEpContextConfig instance. + * + * \param[in] config The OrtEpContextConfig instance to release. May be NULL. + * + * \since Version 1.27. + */ + ORT_CLASS_RELEASE(EpContextConfig); + + /** \brief Read EPContext binary data, using an application read callback or falling back to disk. + * + * If config contains a read callback, the callback is invoked with the provided allocator. Otherwise, ORT reads the + * file from disk. The disk fallback derives the base directory from the graph's model path. + * + * \param[in] config The OrtEpContextConfig from SessionOptions_GetEpContextConfig. May be NULL for disk fallback. + * \param[in] file_name EPContext file name as a null-terminated UTF-8 string. + * \param[in] graph The OrtGraph from which ORT derives the model path for disk fallback. May be NULL. + * \param[in] allocator Allocator for the output buffer. + * \param[out] buffer Output buffer containing the EPContext binary data. + * \param[out] buffer_size Size of the output buffer in bytes. + * + * \snippet{doc} snippets.dox OrtStatus Return Value + * + * \since Version 1.27. + */ + ORT_API2_STATUS(ReadEpContextData, + _In_opt_ const OrtEpContextConfig* config, + _In_ const char* file_name, + _In_opt_ const OrtGraph* graph, + _Inout_ OrtAllocator* allocator, + _Outptr_ void** buffer, + _Out_ size_t* buffer_size); + + /** \brief Write EPContext binary data, using an application write callback or falling back to disk. + * + * If config contains a write callback, the data is forwarded to the application's callback. Otherwise, ORT writes the + * data to disk. The disk fallback derives the base directory from the graph's model path. + * + * \param[in] config The OrtEpContextConfig from SessionOptions_GetEpContextConfig. May be NULL for disk fallback. + * \param[in] file_name EPContext file name as a null-terminated UTF-8 string. + * \param[in] graph The OrtGraph from which ORT derives the model path for disk fallback. May be NULL. + * \param[in] buffer The buffer containing EPContext binary data to write. + * \param[in] buffer_size Size of the buffer in bytes. + * + * \snippet{doc} snippets.dox OrtStatus Return Value + * + * \since Version 1.27. + */ + ORT_API2_STATUS(WriteEpContextData, + _In_opt_ const OrtEpContextConfig* config, + _In_ const char* file_name, + _In_opt_ const OrtGraph* graph, + _In_ const void* buffer, + _In_ size_t buffer_size); }; /** diff --git a/onnxruntime/core/framework/ep_context_options.cc b/onnxruntime/core/framework/ep_context_options.cc index 99fa21b1e4be8..b53a99084152f 100644 --- a/onnxruntime/core/framework/ep_context_options.cc +++ b/onnxruntime/core/framework/ep_context_options.cc @@ -56,6 +56,10 @@ const BufferWriteFuncHolder* ModelGenOptions::TryGetOutputModelWriteFunc() const return std::get_if(&output_model_location); } +const EpContextDataWriteFuncHolder* ModelGenOptions::TryGetEpContextDataWriteFunc() const { + return ep_context_data_write_func.write_func != nullptr ? &ep_context_data_write_func : nullptr; +} + bool ModelGenOptions::AreInitializersEmbeddedInOutputModel() const { return std::holds_alternative(initializers_location); } diff --git a/onnxruntime/core/framework/ep_context_options.h b/onnxruntime/core/framework/ep_context_options.h index 6643516bfb4c3..f05d0d95df73a 100644 --- a/onnxruntime/core/framework/ep_context_options.h +++ b/onnxruntime/core/framework/ep_context_options.h @@ -27,6 +27,14 @@ struct BufferWriteFuncHolder { void* stream_state = nullptr; // Opaque pointer to user's stream state. Passed as first argument to write_func. }; +/// +/// Holds the opaque state and write function that EPs use to write EPContext binary data. +/// +struct EpContextDataWriteFuncHolder { + OrtWriteEpContextDataFunc write_func = nullptr; + void* state = nullptr; +}; + /// /// Holds path and size threshold used to write out initializers to an external file. /// @@ -84,10 +92,13 @@ struct ModelGenOptions { InitializerHandler> // Custom function called for every initializer to determine location. initializers_location = std::monostate{}; + EpContextDataWriteFuncHolder ep_context_data_write_func = {}; + bool HasOutputModelLocation() const; const std::filesystem::path* TryGetOutputModelPath() const; const BufferHolder* TryGetOutputModelBuffer() const; const BufferWriteFuncHolder* TryGetOutputModelWriteFunc() const; + const EpContextDataWriteFuncHolder* TryGetEpContextDataWriteFunc() const; bool AreInitializersEmbeddedInOutputModel() const; const ExternalInitializerFileInfo* TryGetExternalInitializerFileInfo() const; diff --git a/onnxruntime/core/framework/session_options.h b/onnxruntime/core/framework/session_options.h index b328fc916f885..ce406305cc17d 100644 --- a/onnxruntime/core/framework/session_options.h +++ b/onnxruntime/core/framework/session_options.h @@ -226,6 +226,9 @@ struct SessionOptions { bool has_explicit_ep_context_gen_options = false; epctx::ModelGenOptions ep_context_gen_options = {}; epctx::ModelGenOptions GetEpContextGenerationOptions() const; + + OrtReadEpContextDataFunc ep_context_data_read_func = nullptr; + void* ep_context_data_read_state = nullptr; }; inline std::ostream& operator<<(std::ostream& os, const SessionOptions& session_options) { diff --git a/onnxruntime/core/session/abi_session_options.cc b/onnxruntime/core/session/abi_session_options.cc index 3d2d61d409afa..b72dc2c4afa37 100644 --- a/onnxruntime/core/session/abi_session_options.cc +++ b/onnxruntime/core/session/abi_session_options.cc @@ -134,6 +134,18 @@ ORT_API_STATUS_IMPL(OrtApis::GetSessionExecutionMode, _In_ const OrtSessionOptio API_IMPL_END } +ORT_API_STATUS_IMPL(OrtApis::SessionOptions_SetEpContextDataReadFunc, _Inout_ OrtSessionOptions* options, + _In_ OrtReadEpContextDataFunc read_func, _In_opt_ void* state) { + API_IMPL_BEGIN + ORT_API_RETURN_IF(options == nullptr, ORT_INVALID_ARGUMENT, "'options' parameter must not be NULL"); + ORT_API_RETURN_IF(read_func == nullptr, ORT_INVALID_ARGUMENT, "'read_func' parameter must not be NULL"); + + options->value.ep_context_data_read_func = read_func; + options->value.ep_context_data_read_state = state; + return nullptr; + API_IMPL_END +} + // set filepath to save optimized onnx model. ORT_API_STATUS_IMPL(OrtApis::SetOptimizedModelFilePath, _In_ OrtSessionOptions* options, _In_ const ORTCHAR_T* optimized_model_filepath) { options->value.optimized_model_filepath = optimized_model_filepath; diff --git a/onnxruntime/core/session/compile_api.cc b/onnxruntime/core/session/compile_api.cc index 54d26021d8c99..027370af7f76f 100644 --- a/onnxruntime/core/session/compile_api.cc +++ b/onnxruntime/core/session/compile_api.cc @@ -259,6 +259,28 @@ ORT_API_STATUS_IMPL(OrtCompileAPI::ModelCompilationOptions_SetOutputModelGetInit API_IMPL_END } +ORT_API_STATUS_IMPL(OrtCompileAPI::ModelCompilationOptions_SetEpContextDataWriteFunc, + _In_ OrtModelCompilationOptions* ort_model_compile_options, + _In_ OrtWriteEpContextDataFunc write_func, _In_opt_ void* state) { + API_IMPL_BEGIN +#if !defined(ORT_MINIMAL_BUILD) + auto model_compile_options = reinterpret_cast(ort_model_compile_options); + + if (write_func == nullptr) { + return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, "OrtWriteEpContextDataFunc function is null"); + } + + model_compile_options->SetEpContextDataWriteFunc(write_func, state); + return nullptr; +#else + ORT_UNUSED_PARAMETER(ort_model_compile_options); + ORT_UNUSED_PARAMETER(write_func); + ORT_UNUSED_PARAMETER(state); + return OrtApis::CreateStatus(ORT_NOT_IMPLEMENTED, "Compile API is not supported in this build"); +#endif // !defined(ORT_MINIMAL_BUILD) + API_IMPL_END +} + ORT_API_STATUS_IMPL(OrtCompileAPI::ModelCompilationOptions_SetEpContextEmbedMode, _In_ OrtModelCompilationOptions* ort_model_compile_options, bool embed_ep_context_in_model) { @@ -367,6 +389,8 @@ static constexpr OrtCompileApi ort_compile_api = { &OrtCompileAPI::ModelCompilationOptions_SetInputModel, // End of Version 24 - DO NOT MODIFY ABOVE + + &OrtCompileAPI::ModelCompilationOptions_SetEpContextDataWriteFunc, }; // checks that we don't violate the rule that the functions must remain in the slots they were originally assigned diff --git a/onnxruntime/core/session/compile_api.h b/onnxruntime/core/session/compile_api.h index e8f171ee24295..60c75bd5386a9 100644 --- a/onnxruntime/core/session/compile_api.h +++ b/onnxruntime/core/session/compile_api.h @@ -44,5 +44,8 @@ ORT_API_STATUS_IMPL(ModelCompilationOptions_SetOutputModelGetInitializerLocation ORT_API_STATUS_IMPL(ModelCompilationOptions_SetInputModel, _In_ OrtModelCompilationOptions* model_compile_options, _In_ const OrtModel* model); +ORT_API_STATUS_IMPL(ModelCompilationOptions_SetEpContextDataWriteFunc, + _In_ OrtModelCompilationOptions* model_compile_options, + _In_ OrtWriteEpContextDataFunc write_func, _In_opt_ void* state); } // namespace OrtCompileAPI diff --git a/onnxruntime/core/session/model_compilation_options.cc b/onnxruntime/core/session/model_compilation_options.cc index a393bb42fe2cb..607c175f70080 100644 --- a/onnxruntime/core/session/model_compilation_options.cc +++ b/onnxruntime/core/session/model_compilation_options.cc @@ -133,6 +133,13 @@ void ModelCompilationOptions::SetOutputModelGetInitializerLocationFunc( }; } +void ModelCompilationOptions::SetEpContextDataWriteFunc(OrtWriteEpContextDataFunc write_func, void* state) { + session_options_.value.ep_context_gen_options.ep_context_data_write_func = epctx::EpContextDataWriteFuncHolder{ + write_func, + state, + }; +} + Status ModelCompilationOptions::SetEpContextBinaryInformation(const std::filesystem::path& output_directory, const std::filesystem::path& model_name) { if (output_directory.empty() || model_name.empty()) { diff --git a/onnxruntime/core/session/model_compilation_options.h b/onnxruntime/core/session/model_compilation_options.h index 47529e794677e..e24286df2b512 100644 --- a/onnxruntime/core/session/model_compilation_options.h +++ b/onnxruntime/core/session/model_compilation_options.h @@ -97,6 +97,13 @@ class ModelCompilationOptions { void SetOutputModelGetInitializerLocationFunc(OrtGetInitializerLocationFunc get_initializer_location_func, void* state); + /// + /// Sets a user-provided function to handle EPContext binary data writes. + /// + /// The user-provided function called to write EPContext data + /// The user's state. + void SetEpContextDataWriteFunc(OrtWriteEpContextDataFunc write_func, void* state); + /// /// Sets information relate to EP context binary file. /// EP use this information to decide the location and context binary file name. diff --git a/onnxruntime/core/session/onnxruntime_c_api.cc b/onnxruntime/core/session/onnxruntime_c_api.cc index f451eaa401497..60cdcbf760929 100644 --- a/onnxruntime/core/session/onnxruntime_c_api.cc +++ b/onnxruntime/core/session/onnxruntime_c_api.cc @@ -4909,6 +4909,7 @@ static constexpr OrtApi ort_api_1_to_28 = { // End of Version 27 - DO NOT MODIFY ABOVE (see above text for more information) &OrtApis::GetExperimentalFunction, + &OrtApis::SessionOptions_SetEpContextDataReadFunc, }; // OrtApiBase can never change as there is no way to know what version of OrtApiBase is returned by OrtGetApiBase. diff --git a/onnxruntime/core/session/ort_apis.h b/onnxruntime/core/session/ort_apis.h index 61ece2dd9a682..88fe7f1a09466 100644 --- a/onnxruntime/core/session/ort_apis.h +++ b/onnxruntime/core/session/ort_apis.h @@ -66,6 +66,8 @@ ORT_API_STATUS_IMPL(EnableMemPattern, _In_ OrtSessionOptions* options); ORT_API_STATUS_IMPL(DisableMemPattern, _In_ OrtSessionOptions* options); ORT_API_STATUS_IMPL(GetMemPatternEnabled, _In_ const OrtSessionOptions* options, _Out_ int* out); ORT_API_STATUS_IMPL(GetSessionExecutionMode, _In_ const OrtSessionOptions* options, _Out_ ExecutionMode* out); +ORT_API_STATUS_IMPL(SessionOptions_SetEpContextDataReadFunc, _Inout_ OrtSessionOptions* options, + _In_ OrtReadEpContextDataFunc read_func, _In_opt_ void* state); ORT_API_STATUS_IMPL(EnableCpuMemArena, _In_ OrtSessionOptions* options); ORT_API_STATUS_IMPL(DisableCpuMemArena, _In_ OrtSessionOptions* options); ORT_API_STATUS_IMPL(SetSessionLogId, _In_ OrtSessionOptions* options, const char* logid); diff --git a/onnxruntime/core/session/plugin_ep/ep_api.cc b/onnxruntime/core/session/plugin_ep/ep_api.cc index d56f4299402b5..45c0bf9aabd8b 100644 --- a/onnxruntime/core/session/plugin_ep/ep_api.cc +++ b/onnxruntime/core/session/plugin_ep/ep_api.cc @@ -5,11 +5,15 @@ #include #include +#include +#include +#include #include #include #include #include +#include "core/common/path_string.h" #include "core/common/semver.h" #include "core/framework/error_code_helper.h" #include "core/framework/func_api.h" @@ -24,6 +28,7 @@ #include "core/graph/onnx_protobuf.h" #include "core/session/abi_devices.h" #include "core/session/abi_ep_types.h" +#include "core/session/abi_session_options_impl.h" #include "core/session/abi_opschema.h" #include "core/session/environment.h" #include "core/session/onnxruntime_ep_device_ep_metadata_keys.h" @@ -36,7 +41,35 @@ #include "core/session/plugin_ep/ep_event_profiling.h" using namespace onnxruntime; + +struct OrtEpContextConfig { + OrtWriteEpContextDataFunc write_func = nullptr; + void* write_state = nullptr; + OrtReadEpContextDataFunc read_func = nullptr; + void* read_state = nullptr; +}; + namespace OrtExecutionProviderApi { + +namespace { + +std::filesystem::path ResolveEpContextDataPath(const char* file_name, const OrtGraph* graph) { + std::filesystem::path data_path{ToPathString(file_name)}; + if (data_path.is_absolute() || graph == nullptr) { + return data_path; + } + + const ORTCHAR_T* model_path = graph->GetModelPath(); + if (model_path == nullptr || model_path[0] == 0) { + return data_path; + } + + std::filesystem::path model_file_path{model_path}; + return model_file_path.parent_path() / data_path; +} + +} // namespace + ORT_API_STATUS_IMPL(CreateEpDevice, _In_ OrtEpFactory* ep_factory, _In_ const OrtHardwareDevice* hardware_device, _In_opt_ const OrtKeyValuePairs* ep_metadata, @@ -1198,6 +1231,113 @@ ORT_API_STATUS_IMPL(ProfilingEventsContainer_AddEvents, API_IMPL_END } +ORT_API_STATUS_IMPL(SessionOptions_GetEpContextConfig, + _In_ const OrtSessionOptions* session_options, + _Outptr_ OrtEpContextConfig** config) { + API_IMPL_BEGIN + ORT_API_RETURN_IF(session_options == nullptr, ORT_INVALID_ARGUMENT, "OrtSessionOptions is NULL"); + ORT_API_RETURN_IF(config == nullptr, ORT_INVALID_ARGUMENT, "Output OrtEpContextConfig is NULL"); + + auto ep_context_config = std::make_unique(); + if (const auto* write_config = session_options->value.ep_context_gen_options.TryGetEpContextDataWriteFunc()) { + ep_context_config->write_func = write_config->write_func; + ep_context_config->write_state = write_config->state; + } + ep_context_config->read_func = session_options->value.ep_context_data_read_func; + ep_context_config->read_state = session_options->value.ep_context_data_read_state; + + *config = ep_context_config.release(); + return nullptr; + API_IMPL_END +} + +ORT_API(void, ReleaseEpContextConfig, _Frees_ptr_opt_ OrtEpContextConfig* config) { + delete config; +} + +ORT_API_STATUS_IMPL(ReadEpContextData, + _In_opt_ const OrtEpContextConfig* config, + _In_ const char* file_name, + _In_opt_ const OrtGraph* graph, + _Inout_ OrtAllocator* allocator, + _Outptr_ void** buffer, + _Out_ size_t* buffer_size) { + API_IMPL_BEGIN + ORT_API_RETURN_IF(file_name == nullptr, ORT_INVALID_ARGUMENT, "file_name is NULL"); + ORT_API_RETURN_IF(allocator == nullptr, ORT_INVALID_ARGUMENT, "OrtAllocator is NULL"); + ORT_API_RETURN_IF(buffer == nullptr, ORT_INVALID_ARGUMENT, "Output buffer is NULL"); + ORT_API_RETURN_IF(buffer_size == nullptr, ORT_INVALID_ARGUMENT, "Output buffer_size is NULL"); + + *buffer = nullptr; + *buffer_size = 0; + + if (config != nullptr && config->read_func != nullptr) { + OrtStatus* status = config->read_func(config->read_state, file_name, allocator, buffer, buffer_size); + if (status != nullptr) { + return status; + } + + ORT_API_RETURN_IF(*buffer_size != 0 && *buffer == nullptr, ORT_FAIL, + "OrtReadEpContextDataFunc returned a null buffer for non-empty EPContext data"); + return nullptr; + } + + const std::filesystem::path data_path = ResolveEpContextDataPath(file_name, graph); + size_t file_size = 0; + ORT_API_RETURN_IF_STATUS_NOT_OK(Env::Default().GetFileLength(data_path.native().c_str(), file_size)); + + if (file_size == 0) { + return nullptr; + } + + void* allocated_buffer = allocator->Alloc(allocator, file_size); + ORT_API_RETURN_IF(allocated_buffer == nullptr, ORT_FAIL, "Failed to allocate buffer for EPContext data"); + + const auto read_status = Env::Default().ReadFileIntoBuffer( + data_path.native().c_str(), 0, file_size, gsl::make_span(static_cast(allocated_buffer), file_size)); + if (!read_status.IsOK()) { + allocator->Free(allocator, allocated_buffer); + ORT_API_RETURN_IF_STATUS_NOT_OK(read_status); + } + + *buffer = allocated_buffer; + *buffer_size = file_size; + return nullptr; + API_IMPL_END +} + +ORT_API_STATUS_IMPL(WriteEpContextData, + _In_opt_ const OrtEpContextConfig* config, + _In_ const char* file_name, + _In_opt_ const OrtGraph* graph, + _In_ const void* buffer, + _In_ size_t buffer_size) { + API_IMPL_BEGIN + ORT_API_RETURN_IF(file_name == nullptr, ORT_INVALID_ARGUMENT, "file_name is NULL"); + ORT_API_RETURN_IF(buffer_size != 0 && buffer == nullptr, ORT_INVALID_ARGUMENT, + "EPContext data buffer is NULL for non-empty data"); + + if (config != nullptr && config->write_func != nullptr) { + return config->write_func(config->write_state, file_name, buffer, buffer_size); + } + + const std::filesystem::path data_path = ResolveEpContextDataPath(file_name, graph); + std::ofstream output_stream(data_path, std::ios::binary); + ORT_API_RETURN_IF(!output_stream, ORT_FAIL, "Failed to open EPContext data file for write: ", + PathToUTF8String(data_path.native())); + + if (buffer_size != 0) { + ORT_API_RETURN_IF(buffer_size > static_cast(std::numeric_limits::max()), + ORT_INVALID_ARGUMENT, "EPContext data buffer is too large to write"); + output_stream.write(static_cast(buffer), static_cast(buffer_size)); + ORT_API_RETURN_IF(!output_stream, ORT_FAIL, "Failed to write EPContext data file: ", + PathToUTF8String(data_path.native())); + } + + return nullptr; + API_IMPL_END +} + static constexpr OrtEpApi ort_ep_api = { // NOTE: ABI compatibility depends on the order within this struct so all additions must be at the end, // and no functions can be removed (the implementation needs to change to return an error). @@ -1287,6 +1427,11 @@ static constexpr OrtEpApi ort_ep_api = { &OrtExecutionProviderApi::ProfilingEvent_GetArgValue, &OrtExecutionProviderApi::ProfilingEventsContainer_AddEvents, // End of Version 25 - DO NOT MODIFY ABOVE + + &OrtExecutionProviderApi::SessionOptions_GetEpContextConfig, + &OrtExecutionProviderApi::ReleaseEpContextConfig, + &OrtExecutionProviderApi::ReadEpContextData, + &OrtExecutionProviderApi::WriteEpContextData, }; // checks that we don't violate the rule that the functions must remain in the slots they were originally assigned diff --git a/onnxruntime/core/session/plugin_ep/ep_api.h b/onnxruntime/core/session/plugin_ep/ep_api.h index e32e267a75ba5..8ca542b59a425 100644 --- a/onnxruntime/core/session/plugin_ep/ep_api.h +++ b/onnxruntime/core/session/plugin_ep/ep_api.h @@ -179,4 +179,23 @@ ORT_API_STATUS_IMPL(ProfilingEvent_GetDurationUs, _In_ const OrtProfilingEvent* ORT_API_STATUS_IMPL(ProfilingEvent_GetArgValue, _In_ const OrtProfilingEvent* event, _In_ const char* key, _Outptr_result_maybenull_ const char** out); +// EPContext data I/O +ORT_API_STATUS_IMPL(SessionOptions_GetEpContextConfig, + _In_ const OrtSessionOptions* session_options, + _Outptr_ OrtEpContextConfig** config); +ORT_API(void, ReleaseEpContextConfig, _Frees_ptr_opt_ OrtEpContextConfig* config); +ORT_API_STATUS_IMPL(ReadEpContextData, + _In_opt_ const OrtEpContextConfig* config, + _In_ const char* file_name, + _In_opt_ const OrtGraph* graph, + _Inout_ OrtAllocator* allocator, + _Outptr_ void** buffer, + _Out_ size_t* buffer_size); +ORT_API_STATUS_IMPL(WriteEpContextData, + _In_opt_ const OrtEpContextConfig* config, + _In_ const char* file_name, + _In_opt_ const OrtGraph* graph, + _In_ const void* buffer, + _In_ size_t buffer_size); + } // namespace OrtExecutionProviderApi diff --git a/onnxruntime/test/framework/ep_plugin_provider_test.cc b/onnxruntime/test/framework/ep_plugin_provider_test.cc index fef185e20f341..0cceee4cc6576 100644 --- a/onnxruntime/test/framework/ep_plugin_provider_test.cc +++ b/onnxruntime/test/framework/ep_plugin_provider_test.cc @@ -6,12 +6,14 @@ #include #include #include +#include #include #include #include #include "gsl/gsl" #include "gtest/gtest.h" +#include "core/common/path_string.h" #include "core/common/logging/sinks/file_sink.h" #include "core/framework/config_options.h" #include "core/framework/kernel_def_builder.h" @@ -22,6 +24,7 @@ #include "core/graph/model.h" #include "core/optimizer/graph_optimizer_registry.h" #include "core/session/abi_devices.h" +#include "core/session/model_compilation_options.h" #include "core/session/onnxruntime_cxx_api.h" #include "core/session/onnxruntime_session_options_config_keys.h" #include "test/util/include/api_asserts.h" @@ -58,6 +61,52 @@ static void CheckFileIsEmpty(const PathString& filename) { EXPECT_TRUE(content.empty()); } +struct EpContextReadCallbackState { + bool called = false; + std::string file_name; + std::vector payload; +}; + +static OrtStatus* ORT_API_CALL EpContextReadCallback(void* state, const char* file_name, OrtAllocator* allocator, + void** buffer, size_t* data_size) { + auto* read_state = static_cast(state); + read_state->called = true; + read_state->file_name = file_name; + + *buffer = nullptr; + *data_size = read_state->payload.size(); + + if (read_state->payload.empty()) { + return nullptr; + } + + OrtStatus* status = Ort::GetApi().AllocatorAlloc(allocator, read_state->payload.size(), buffer); + if (status != nullptr) { + return status; + } + + std::memcpy(*buffer, read_state->payload.data(), read_state->payload.size()); + return nullptr; +} + +struct EpContextWriteCallbackState { + bool called = false; + std::string file_name; + std::vector payload; +}; + +static OrtStatus* ORT_API_CALL EpContextWriteCallback(void* state, const char* file_name, const void* buffer, + size_t buffer_size) { + auto* write_state = static_cast(state); + write_state->called = true; + write_state->file_name = file_name; + write_state->payload.clear(); + if (buffer_size != 0) { + write_state->payload.assign(static_cast(buffer), static_cast(buffer) + buffer_size); + } + return nullptr; +} + // Normally, a plugin EP would be implemented in a separate library. // The `test_plugin_ep` namespace contains a local implementation intended for unit testing. namespace test_plugin_ep { @@ -1718,6 +1767,89 @@ TEST(PluginExecutionProviderTest, GetGraphCaptureNodeAssignmentPolicy) { } } +TEST(PluginExecutionProviderTest, EpContextDataReadFuncIsCalledViaEpApi) { + const auto& ep_api = Ort::GetEpApi(); + Ort::SessionOptions session_options; + + EpContextReadCallbackState read_state{ + false, + {}, + {'e', 'p', 'c', 't', 'x'}, + }; + session_options.SetEpContextDataReadFunc(EpContextReadCallback, &read_state); + + OrtEpContextConfig* ep_context_config = nullptr; + ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(session_options, &ep_context_config)); + auto release_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(ep_context_config); }); + + Ort::AllocatorWithDefaultOptions allocator; + void* buffer = nullptr; + size_t buffer_size = 0; + ASSERT_ORTSTATUS_OK(ep_api.ReadEpContextData(ep_context_config, "context.bin", nullptr, allocator, + &buffer, &buffer_size)); + auto release_buffer = gsl::finally([&]() { allocator.Free(buffer); }); + + ASSERT_TRUE(read_state.called); + EXPECT_EQ(read_state.file_name, "context.bin"); + ASSERT_EQ(buffer_size, read_state.payload.size()); + EXPECT_EQ(std::vector(static_cast(buffer), static_cast(buffer) + buffer_size), + read_state.payload); +} + +#if !defined(ORT_MINIMAL_BUILD) +TEST(PluginExecutionProviderTest, EpContextDataWriteFuncIsCalledViaEpApi) { + const auto& ep_api = Ort::GetEpApi(); + Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "EpContextDataWriteFuncIsCalledViaEpApi"}; + Ort::SessionOptions session_options; + Ort::ModelCompilationOptions compilation_options{env, session_options}; + + EpContextWriteCallbackState write_state{}; + compilation_options.SetEpContextDataWriteFunc(EpContextWriteCallback, &write_state); + + const auto* internal_options = reinterpret_cast( + static_cast(compilation_options)); + OrtEpContextConfig* ep_context_config = nullptr; + ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(&internal_options->GetSessionOptions(), + &ep_context_config)); + auto release_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(ep_context_config); }); + + const std::vector payload{'b', 'i', 'n', 'a', 'r', 'y'}; + ASSERT_ORTSTATUS_OK(ep_api.WriteEpContextData(ep_context_config, "engine.bin", nullptr, + payload.data(), payload.size())); + + ASSERT_TRUE(write_state.called); + EXPECT_EQ(write_state.file_name, "engine.bin"); + EXPECT_EQ(write_state.payload, payload); +} +#endif // !defined(ORT_MINIMAL_BUILD) + +TEST(PluginExecutionProviderTest, EpContextDataFallsBackToDisk) { + const auto& ep_api = Ort::GetEpApi(); + const std::filesystem::path test_dir = std::filesystem::temp_directory_path() / "ort_ep_context_data_test"; + std::filesystem::create_directories(test_dir); + const std::filesystem::path data_path = test_dir / "context.bin"; + const std::string data_path_utf8 = PathToUTF8String(data_path.native()); + auto cleanup = gsl::finally([&]() { + std::error_code ec; + std::filesystem::remove(data_path, ec); + std::filesystem::remove(test_dir, ec); + }); + + const std::vector payload{'d', 'i', 's', 'k'}; + ASSERT_ORTSTATUS_OK(ep_api.WriteEpContextData(nullptr, data_path_utf8.c_str(), nullptr, + payload.data(), payload.size())); + + Ort::AllocatorWithDefaultOptions allocator; + void* buffer = nullptr; + size_t buffer_size = 0; + ASSERT_ORTSTATUS_OK(ep_api.ReadEpContextData(nullptr, data_path_utf8.c_str(), nullptr, allocator, + &buffer, &buffer_size)); + auto release_buffer = gsl::finally([&]() { allocator.Free(buffer); }); + + ASSERT_EQ(buffer_size, payload.size()); + EXPECT_EQ(std::vector(static_cast(buffer), static_cast(buffer) + buffer_size), payload); +} + // Helper: create a no-threshold resource accountant via the real factory (config ","). static IResourceAccountant* CreateNoThresholdAccountant(std::optional& acc_map) { ConfigOptions config; From 19b36c020af5f746028339bfabe7f63865e2b830 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Fri, 15 May 2026 16:56:37 -0700 Subject: [PATCH 02/48] Add EPContext data callback test coverage --- onnxruntime/core/session/compile_api.cc | 6 +- .../autoep/library/example_plugin_ep/ep.cc | 46 +++- .../autoep/library/example_plugin_ep/ep.h | 8 +- .../library/example_plugin_ep/ep_factory.cc | 16 +- onnxruntime/test/autoep/test_execution.cc | 116 +++++++++ .../test/framework/ep_plugin_provider_test.cc | 246 ++++++++++++++++++ 6 files changed, 427 insertions(+), 11 deletions(-) diff --git a/onnxruntime/core/session/compile_api.cc b/onnxruntime/core/session/compile_api.cc index 027370af7f76f..cdcbe7f3e867b 100644 --- a/onnxruntime/core/session/compile_api.cc +++ b/onnxruntime/core/session/compile_api.cc @@ -266,6 +266,10 @@ ORT_API_STATUS_IMPL(OrtCompileAPI::ModelCompilationOptions_SetEpContextDataWrite #if !defined(ORT_MINIMAL_BUILD) auto model_compile_options = reinterpret_cast(ort_model_compile_options); + if (model_compile_options == nullptr) { + return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, "OrtModelCompilationOptions is NULL"); + } + if (write_func == nullptr) { return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, "OrtWriteEpContextDataFunc function is null"); } @@ -390,7 +394,7 @@ static constexpr OrtCompileApi ort_compile_api = { &OrtCompileAPI::ModelCompilationOptions_SetInputModel, // End of Version 24 - DO NOT MODIFY ABOVE - &OrtCompileAPI::ModelCompilationOptions_SetEpContextDataWriteFunc, + &OrtCompileAPI::ModelCompilationOptions_SetEpContextDataWriteFunc, }; // checks that we don't violate the rule that the functions must remain in the slots they were originally assigned diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc index fecf7ac9a4038..7a05b921230bd 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc @@ -167,13 +167,15 @@ struct EpContextNodeComputeInfo : NodeComputeInfoBase { ExampleEp& ep; }; -ExampleEp::ExampleEp(ExampleEpFactory& factory, const std::string& name, const Config& config, const OrtLogger& logger) +ExampleEp::ExampleEp(ExampleEpFactory& factory, const std::string& name, const Config& config, const OrtLogger& logger, + OrtEpContextConfig* ep_context_config) : OrtEp{}, // explicitly call the struct ctor to ensure all optional values are default initialized ApiPtrs{static_cast(factory)}, factory_{factory}, name_{name}, config_{config}, - logger_{logger} { + logger_{logger}, + ep_context_config_{ep_context_config} { ort_version_supported = ORT_API_VERSION; // set to the ORT version we were compiled with. // Initialize the execution provider's function table @@ -193,7 +195,9 @@ ExampleEp::ExampleEp(ExampleEpFactory& factory, const std::string& name, const C ORT_FILE, __LINE__, __FUNCTION__)); } -ExampleEp::~ExampleEp() = default; +ExampleEp::~ExampleEp() { + ep_api.ReleaseEpContextConfig(ep_context_config_); +} /*static*/ const char* ORT_API_CALL ExampleEp ::GetNameImpl(const OrtEp* this_ptr) noexcept { @@ -409,6 +413,28 @@ OrtStatus* ORT_API_CALL ExampleEp::CompileImpl(_In_ OrtEp* this_ptr, _In_ const auto fused_node_name = fused_node.GetName(); if (is_ep_context_node) { + Ort::ConstOpAttr embed_mode_attr; + RETURN_IF_ERROR(nodes[0].GetAttributeByName("embed_mode", embed_mode_attr)); + int64_t embed_mode = 1; + RETURN_IF_ERROR(embed_mode_attr.GetValue(embed_mode)); + + if (embed_mode == 0) { + Ort::ConstOpAttr ep_cache_context_attr; + RETURN_IF_ERROR(nodes[0].GetAttributeByName("ep_cache_context", ep_cache_context_attr)); + std::string ep_cache_context; + RETURN_IF_ERROR(ep_cache_context_attr.GetValue(ep_cache_context)); + + Ort::AllocatorWithDefaultOptions allocator; + void* ep_context_data = nullptr; + size_t ep_context_data_size = 0; + RETURN_IF_ERROR(ep->ep_api.ReadEpContextData(ep->ep_context_config_, ep_cache_context.c_str(), ort_graphs[0], + allocator, &ep_context_data, &ep_context_data_size)); + (void)ep_context_data_size; + if (ep_context_data != nullptr) { + allocator.Free(ep_context_data); + } + } + // Create EpContextKernel for EPContext nodes - clearly separates from MulKernel ep->ep_context_kernels_.emplace(fused_node_name, std::make_unique(ep->ort_api, ep->logger_)); @@ -449,7 +475,7 @@ OrtStatus* ORT_API_CALL ExampleEp::CompileImpl(_In_ OrtEp* this_ptr, _In_ const // Create EpContext nodes for the fused nodes we compiled (only for Mul, not EPContext). if (ep->config_.enable_ep_context) { assert(ep_context_nodes != nullptr); - RETURN_IF_ERROR(ep->CreateEpContextNodes(gsl::span(fused_nodes, count), + RETURN_IF_ERROR(ep->CreateEpContextNodes(ort_graphs[0], gsl::span(fused_nodes, count), gsl::span(ep_context_nodes, count))); } } @@ -479,7 +505,8 @@ void ORT_API_CALL ExampleEp::ReleaseNodeComputeInfosImpl(OrtEp* this_ptr, // Creates EPContext nodes from the given fused nodes. // This is an example implementation that can be used to generate an EPContext model. However, this example EP // cannot currently run the EPContext model. -OrtStatus* ExampleEp::CreateEpContextNodes(gsl::span fused_nodes, +OrtStatus* ExampleEp::CreateEpContextNodes(const OrtGraph* graph, + gsl::span fused_nodes, /*out*/ gsl::span ep_context_nodes) { try { assert(fused_nodes.size() == ep_context_nodes.size()); @@ -512,11 +539,16 @@ OrtStatus* ExampleEp::CreateEpContextNodes(gsl::span fused_nodes collect_input_output_names(fused_node_outputs, /*out*/ output_names); int64_t is_main_context = (i == 0); - int64_t embed_mode = 1; + int64_t embed_mode = config_.embed_ep_context_in_model ? 1 : 0; // Create node attributes. The CreateNode() function copies the attributes. std::array attributes = {}; - std::string ep_ctx = "binary_data"; + std::string ep_ctx = config_.embed_ep_context_in_model ? "binary_data" : fused_node_name + ".ctx"; + if (!config_.embed_ep_context_in_model) { + const std::string ep_context_data = "binary_data"; + RETURN_IF_ERROR(ep_api.WriteEpContextData(ep_context_config_, ep_ctx.c_str(), graph, + ep_context_data.data(), ep_context_data.size())); + } attributes[0] = Ort::OpAttr("ep_cache_context", ep_ctx.data(), static_cast(ep_ctx.size()), ORT_OP_ATTR_STRING); diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep.h b/onnxruntime/test/autoep/library/example_plugin_ep/ep.h index 5dcd9f07bef1f..8f6080fe3d46a 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep.h +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep.h @@ -61,11 +61,13 @@ class ExampleEp : public OrtEp, public ApiPtrs { public: struct Config { bool enable_ep_context = false; + bool embed_ep_context_in_model = true; bool enable_weightless_ep_context_nodes = false; // Other EP configs (typically extracted from OrtSessionOptions or OrtHardwareDevice(s)) }; - ExampleEp(ExampleEpFactory& factory, const std::string& name, const Config& config, const OrtLogger& logger); + ExampleEp(ExampleEpFactory& factory, const std::string& name, const Config& config, const OrtLogger& logger, + OrtEpContextConfig* ep_context_config); ~ExampleEp(); @@ -108,7 +110,8 @@ class ExampleEp : public OrtEp, public ApiPtrs { static OrtStatus* ORT_API_CALL GetDefaultMemoryDeviceImpl(_In_ const OrtEp* this_ptr, _Outptr_ const OrtMemoryDevice** device) noexcept; - OrtStatus* CreateEpContextNodes(gsl::span fused_nodes, + OrtStatus* CreateEpContextNodes(const OrtGraph* graph, + gsl::span fused_nodes, /*out*/ gsl::span ep_context_nodes); // Returns true if the EP should save constant initializers so that they are available during inference. @@ -122,6 +125,7 @@ class ExampleEp : public OrtEp, public ApiPtrs { std::string name_; Config config_{}; const OrtLogger& logger_; + OrtEpContextConfig* ep_context_config_ = nullptr; std::unordered_map> mul_kernels_; std::unordered_map> ep_context_kernels_; std::unordered_map float_initializers_; diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc index 875f70bd29f3c..1d8f3e4a11fe9 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc @@ -219,17 +219,31 @@ OrtStatus* ORT_API_CALL ExampleEpFactory::CreateEpImpl(OrtEpFactory* this_ptr, // Create EP configuration from session options, if needed. // Note: should not store a direct reference to the session options object as its lifespan is not guaranteed. std::string ep_context_enable; + std::string ep_context_embed_mode; std::string weightless_ep_context_nodes_enable; RETURN_IF_ERROR(GetSessionConfigEntryOrDefault(*session_options, kOrtSessionOptionEpContextEnable, "0", ep_context_enable)); + RETURN_IF_ERROR(GetSessionConfigEntryOrDefault(*session_options, kOrtSessionOptionEpContextEmbedMode, "1", + ep_context_embed_mode)); RETURN_IF_ERROR(GetSessionConfigEntryOrDefault(*session_options, kOrtSessionOptionEpEnableWeightlessEpContextNodes, "0", weightless_ep_context_nodes_enable)); ExampleEp::Config config = {}; config.enable_ep_context = ep_context_enable == "1"; + config.embed_ep_context_in_model = ep_context_embed_mode != "0"; config.enable_weightless_ep_context_nodes = weightless_ep_context_nodes_enable == "1"; - auto dummy_ep = std::make_unique(*factory, factory->ep_name_, config, *logger); + auto release_ep_context_config = [factory](OrtEpContextConfig* config_to_release) { + factory->ep_api.ReleaseEpContextConfig(config_to_release); + }; + std::unique_ptr ep_context_config{ + nullptr, release_ep_context_config}; + OrtEpContextConfig* ep_context_config_raw = nullptr; + RETURN_IF_ERROR(factory->ep_api.SessionOptions_GetEpContextConfig(session_options, &ep_context_config_raw)); + ep_context_config.reset(ep_context_config_raw); + + auto dummy_ep = std::make_unique(*factory, factory->ep_name_, config, *logger, + ep_context_config.release()); *ep = dummy_ep.release(); return nullptr; diff --git a/onnxruntime/test/autoep/test_execution.cc b/onnxruntime/test/autoep/test_execution.cc index 93633f9a375bb..35cbdc9009acc 100644 --- a/onnxruntime/test/autoep/test_execution.cc +++ b/onnxruntime/test/autoep/test_execution.cc @@ -1,9 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +#include #include #include #include +#include #include // #include #include @@ -29,6 +31,47 @@ namespace test { namespace { +struct EpContextDataCallbackState { + bool write_called = false; + bool read_called = false; + std::string write_file_name; + std::string read_file_name; + std::vector payload; +}; + +OrtStatus* ORT_API_CALL StoreEpContextDataCallback(void* state, const char* file_name, const void* buffer, + size_t buffer_size) { + auto* callback_state = static_cast(state); + callback_state->write_called = true; + callback_state->write_file_name = file_name; + callback_state->payload.clear(); + if (buffer_size != 0) { + callback_state->payload.assign(static_cast(buffer), static_cast(buffer) + buffer_size); + } + return nullptr; +} + +OrtStatus* ORT_API_CALL LoadEpContextDataCallback(void* state, const char* file_name, OrtAllocator* allocator, + void** buffer, size_t* data_size) { + auto* callback_state = static_cast(state); + callback_state->read_called = true; + callback_state->read_file_name = file_name; + + *buffer = nullptr; + *data_size = callback_state->payload.size(); + if (callback_state->payload.empty()) { + return nullptr; + } + + OrtStatus* status = Ort::GetApi().AllocatorAlloc(allocator, callback_state->payload.size(), buffer); + if (status != nullptr) { + return status; + } + + std::copy(callback_state->payload.begin(), callback_state->payload.end(), static_cast(*buffer)); + return nullptr; +} + void RunMulModelWithPluginEp(const ORTCHAR_T* model_path, const Ort::SessionOptions& session_options) { Ort::Session session(*ort_env, model_path, session_options); @@ -521,6 +564,79 @@ TEST(OrtEpLibrary, PluginEp_GenEpContextModel) { } } +TEST(OrtEpLibrary, PluginEp_GenEpContextModel_ExternalDataUsesWriteCallback) { + RegisteredEpDeviceUniquePtr example_ep; + ASSERT_NO_FATAL_FAILURE(Utils::RegisterAndGetExampleEp(*ort_env, Utils::example_ep_info, example_ep)); + Ort::ConstEpDevice plugin_ep_device(example_ep.get()); + + const ORTCHAR_T* input_model_file = ORT_TSTR("testdata/mul_1.onnx"); + const ORTCHAR_T* output_model_file = ORT_TSTR("plugin_ep_mul_1_external_ctx.onnx"); + std::filesystem::remove(output_model_file); + auto cleanup = gsl::finally([&]() { std::filesystem::remove(output_model_file); }); + + Ort::SessionOptions session_options; + std::unordered_map ep_options; + session_options.AppendExecutionProvider_V2(*ort_env, {plugin_ep_device}, ep_options); + + EpContextDataCallbackState callback_state; + Ort::ModelCompilationOptions compile_options(*ort_env, session_options); + compile_options.SetFlags(OrtCompileApiFlags_ERROR_IF_NO_NODES_COMPILED); + compile_options.SetInputModelPath(input_model_file); + compile_options.SetOutputModelPath(output_model_file); + compile_options.SetEpContextEmbedMode(false); + compile_options.SetEpContextDataWriteFunc(StoreEpContextDataCallback, &callback_state); + + ASSERT_CXX_ORTSTATUS_OK(Ort::CompileModel(*ort_env, compile_options)); + ASSERT_TRUE(std::filesystem::exists(output_model_file)); + ASSERT_TRUE(callback_state.write_called); + EXPECT_FALSE(callback_state.write_file_name.empty()); + EXPECT_EQ(std::string(callback_state.payload.begin(), callback_state.payload.end()), "binary_data"); +} + +TEST(OrtEpLibrary, PluginEp_LoadEpContextModel_ExternalDataUsesReadCallback) { + RegisteredEpDeviceUniquePtr example_ep; + ASSERT_NO_FATAL_FAILURE(Utils::RegisterAndGetExampleEp(*ort_env, Utils::example_ep_info, example_ep)); + Ort::ConstEpDevice plugin_ep_device(example_ep.get()); + + const ORTCHAR_T* input_model_file = ORT_TSTR("testdata/mul_1.onnx"); + const ORTCHAR_T* compiled_model_file = ORT_TSTR("plugin_ep_mul_1_external_ctx_load.onnx"); + std::filesystem::remove(compiled_model_file); + auto cleanup = gsl::finally([&]() { std::filesystem::remove(compiled_model_file); }); + + EpContextDataCallbackState write_callback_state; + { + Ort::SessionOptions session_options; + std::unordered_map ep_options; + session_options.AppendExecutionProvider_V2(*ort_env, {plugin_ep_device}, ep_options); + + Ort::ModelCompilationOptions compile_options(*ort_env, session_options); + compile_options.SetFlags(OrtCompileApiFlags_ERROR_IF_NO_NODES_COMPILED); + compile_options.SetInputModelPath(input_model_file); + compile_options.SetOutputModelPath(compiled_model_file); + compile_options.SetEpContextEmbedMode(false); + compile_options.SetEpContextDataWriteFunc(StoreEpContextDataCallback, &write_callback_state); + + ASSERT_CXX_ORTSTATUS_OK(Ort::CompileModel(*ort_env, compile_options)); + ASSERT_TRUE(std::filesystem::exists(compiled_model_file)); + ASSERT_TRUE(write_callback_state.write_called); + } + + EpContextDataCallbackState read_callback_state; + read_callback_state.payload = write_callback_state.payload; + { + Ort::SessionOptions session_options; + session_options.SetEpContextDataReadFunc(LoadEpContextDataCallback, &read_callback_state); + + std::unordered_map ep_options; + session_options.AppendExecutionProvider_V2(*ort_env, {plugin_ep_device}, ep_options); + + Ort::Session session(*ort_env, compiled_model_file, session_options); + } + + ASSERT_TRUE(read_callback_state.read_called); + EXPECT_EQ(read_callback_state.read_file_name, write_callback_state.write_file_name); +} + TEST(OrtEpLibrary, PluginEp_GenWeightlessEpContextModel) { RegisteredEpDeviceUniquePtr example_ep; ASSERT_NO_FATAL_FAILURE(Utils::RegisterAndGetExampleEp(*ort_env, Utils::example_ep_info, example_ep)); diff --git a/onnxruntime/test/framework/ep_plugin_provider_test.cc b/onnxruntime/test/framework/ep_plugin_provider_test.cc index 0cceee4cc6576..1cf183873eaae 100644 --- a/onnxruntime/test/framework/ep_plugin_provider_test.cc +++ b/onnxruntime/test/framework/ep_plugin_provider_test.cc @@ -20,6 +20,7 @@ #include "core/framework/op_kernel.h" #include "core/framework/resource_accountant.h" #include "core/graph/constants.h" +#include "core/graph/ep_api_types.h" #include "core/graph/graph_viewer.h" #include "core/graph/model.h" #include "core/optimizer/graph_optimizer_registry.h" @@ -61,6 +62,25 @@ static void CheckFileIsEmpty(const PathString& filename) { EXPECT_TRUE(content.empty()); } +static void ExpectOrtStatus(OrtStatus* status_ptr, OrtErrorCode expected_code, const char* expected_message) { + Ort::Status status{status_ptr}; + ASSERT_FALSE(status.IsOK()); + EXPECT_EQ(status.GetErrorCode(), expected_code); + EXPECT_THAT(status.GetErrorMessage(), ::testing::HasSubstr(expected_message)); +} + +static void ExpectOrtStatusNotOk(OrtStatus* status_ptr) { + Ort::Status status{status_ptr}; + EXPECT_FALSE(status.IsOK()); +} + +static std::filesystem::path MakeEpContextDataTestDir(const char* test_name) { + std::filesystem::path test_dir = std::filesystem::temp_directory_path() / test_name; + std::filesystem::remove_all(test_dir); + std::filesystem::create_directories(test_dir); + return test_dir; +} + struct EpContextReadCallbackState { bool called = false; std::string file_name; @@ -89,6 +109,26 @@ static OrtStatus* ORT_API_CALL EpContextReadCallback(void* state, const char* fi return nullptr; } +struct EpContextCallbackErrorState { + OrtErrorCode error_code = ORT_FAIL; + const char* message = nullptr; +}; + +static OrtStatus* ORT_API_CALL EpContextFailingReadCallback(void* state, const char* /*file_name*/, + OrtAllocator* /*allocator*/, void** /*buffer*/, + size_t* /*data_size*/) { + const auto* error_state = static_cast(state); + return Ort::GetApi().CreateStatus(error_state->error_code, error_state->message); +} + +static OrtStatus* ORT_API_CALL EpContextNonEmptyNullBufferReadCallback(void* /*state*/, const char* /*file_name*/, + OrtAllocator* /*allocator*/, void** buffer, + size_t* data_size) { + *buffer = nullptr; + *data_size = 4; + return nullptr; +} + struct EpContextWriteCallbackState { bool called = false; std::string file_name; @@ -107,6 +147,12 @@ static OrtStatus* ORT_API_CALL EpContextWriteCallback(void* state, const char* f return nullptr; } +static OrtStatus* ORT_API_CALL EpContextFailingWriteCallback(void* state, const char* /*file_name*/, + const void* /*buffer*/, size_t /*buffer_size*/) { + const auto* error_state = static_cast(state); + return Ort::GetApi().CreateStatus(error_state->error_code, error_state->message); +} + // Normally, a plugin EP would be implemented in a separate library. // The `test_plugin_ep` namespace contains a local implementation intended for unit testing. namespace test_plugin_ep { @@ -1796,6 +1842,146 @@ TEST(PluginExecutionProviderTest, EpContextDataReadFuncIsCalledViaEpApi) { read_state.payload); } +TEST(PluginExecutionProviderTest, EpContextDataApiRejectsInvalidArguments) { + const auto& ort_api = Ort::GetApi(); + const auto& ep_api = Ort::GetEpApi(); + + Ort::SessionOptions session_options; + OrtEpContextConfig* ep_context_config = nullptr; + ExpectOrtStatus(ep_api.SessionOptions_GetEpContextConfig(nullptr, &ep_context_config), ORT_INVALID_ARGUMENT, + "OrtSessionOptions is NULL"); + ExpectOrtStatus(ep_api.SessionOptions_GetEpContextConfig(session_options, nullptr), ORT_INVALID_ARGUMENT, + "Output OrtEpContextConfig is NULL"); + + ExpectOrtStatus(ort_api.SessionOptions_SetEpContextDataReadFunc(nullptr, EpContextReadCallback, nullptr), + ORT_INVALID_ARGUMENT, "'options' parameter must not be NULL"); + ExpectOrtStatus(ort_api.SessionOptions_SetEpContextDataReadFunc(session_options, nullptr, nullptr), + ORT_INVALID_ARGUMENT, "'read_func' parameter must not be NULL"); + + Ort::AllocatorWithDefaultOptions allocator; + void* buffer = nullptr; + size_t buffer_size = 0; + ExpectOrtStatus(ep_api.ReadEpContextData(nullptr, nullptr, nullptr, allocator, &buffer, &buffer_size), + ORT_INVALID_ARGUMENT, "file_name is NULL"); + ExpectOrtStatus(ep_api.ReadEpContextData(nullptr, "context.bin", nullptr, nullptr, &buffer, &buffer_size), + ORT_INVALID_ARGUMENT, "OrtAllocator is NULL"); + ExpectOrtStatus(ep_api.ReadEpContextData(nullptr, "context.bin", nullptr, allocator, nullptr, &buffer_size), + ORT_INVALID_ARGUMENT, "Output buffer is NULL"); + ExpectOrtStatus(ep_api.ReadEpContextData(nullptr, "context.bin", nullptr, allocator, &buffer, nullptr), + ORT_INVALID_ARGUMENT, "Output buffer_size is NULL"); + + const std::vector payload{'x'}; + ExpectOrtStatus(ep_api.WriteEpContextData(nullptr, nullptr, nullptr, payload.data(), payload.size()), + ORT_INVALID_ARGUMENT, "file_name is NULL"); + ExpectOrtStatus(ep_api.WriteEpContextData(nullptr, "context.bin", nullptr, nullptr, payload.size()), + ORT_INVALID_ARGUMENT, "EPContext data buffer is NULL for non-empty data"); + +#if !defined(ORT_MINIMAL_BUILD) + Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "EpContextDataApiRejectsInvalidArguments"}; + Ort::ModelCompilationOptions compilation_options{env, session_options}; + const auto& compile_api = Ort::GetCompileApi(); + ExpectOrtStatus(compile_api.ModelCompilationOptions_SetEpContextDataWriteFunc(nullptr, EpContextWriteCallback, + nullptr), + ORT_INVALID_ARGUMENT, "OrtModelCompilationOptions is NULL"); + ExpectOrtStatus(compile_api.ModelCompilationOptions_SetEpContextDataWriteFunc(compilation_options, nullptr, + nullptr), + ORT_INVALID_ARGUMENT, "OrtWriteEpContextDataFunc function is null"); +#endif // !defined(ORT_MINIMAL_BUILD) +} + +TEST(PluginExecutionProviderTest, EpContextDataCallbackErrorsArePropagated) { + const auto& ep_api = Ort::GetEpApi(); + Ort::SessionOptions session_options; + + EpContextCallbackErrorState read_error{ORT_FAIL, "read callback failed"}; + session_options.SetEpContextDataReadFunc(EpContextFailingReadCallback, &read_error); + + OrtEpContextConfig* ep_context_config = nullptr; + ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(session_options, &ep_context_config)); + auto release_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(ep_context_config); }); + + Ort::AllocatorWithDefaultOptions allocator; + void* buffer = nullptr; + size_t buffer_size = 0; + ExpectOrtStatus(ep_api.ReadEpContextData(ep_context_config, "context.bin", nullptr, allocator, + &buffer, &buffer_size), + ORT_FAIL, "read callback failed"); + +#if !defined(ORT_MINIMAL_BUILD) + Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "EpContextDataCallbackErrorsArePropagated"}; + Ort::ModelCompilationOptions compilation_options{env, session_options}; + EpContextCallbackErrorState write_error{ORT_EP_FAIL, "write callback failed"}; + compilation_options.SetEpContextDataWriteFunc(EpContextFailingWriteCallback, &write_error); + + const auto* internal_options = reinterpret_cast( + static_cast(compilation_options)); + OrtEpContextConfig* write_config = nullptr; + ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(&internal_options->GetSessionOptions(), &write_config)); + auto release_write_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(write_config); }); + + const std::vector payload{'x'}; + ExpectOrtStatus(ep_api.WriteEpContextData(write_config, "context.bin", nullptr, payload.data(), payload.size()), + ORT_EP_FAIL, "write callback failed"); +#endif // !defined(ORT_MINIMAL_BUILD) +} + +TEST(PluginExecutionProviderTest, EpContextDataAllowsEmptyPayloads) { + const auto& ep_api = Ort::GetEpApi(); + Ort::SessionOptions session_options; + + EpContextReadCallbackState read_state{}; + session_options.SetEpContextDataReadFunc(EpContextReadCallback, &read_state); + + OrtEpContextConfig* ep_context_config = nullptr; + ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(session_options, &ep_context_config)); + auto release_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(ep_context_config); }); + + Ort::AllocatorWithDefaultOptions allocator; + void* buffer = reinterpret_cast(0x1); + size_t buffer_size = 1; + ASSERT_ORTSTATUS_OK(ep_api.ReadEpContextData(ep_context_config, "empty.bin", nullptr, allocator, + &buffer, &buffer_size)); + EXPECT_TRUE(read_state.called); + EXPECT_EQ(read_state.file_name, "empty.bin"); + EXPECT_EQ(buffer, nullptr); + EXPECT_EQ(buffer_size, 0U); + +#if !defined(ORT_MINIMAL_BUILD) + Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "EpContextDataAllowsEmptyPayloads"}; + Ort::ModelCompilationOptions compilation_options{env, session_options}; + EpContextWriteCallbackState write_state{}; + compilation_options.SetEpContextDataWriteFunc(EpContextWriteCallback, &write_state); + + const auto* internal_options = reinterpret_cast( + static_cast(compilation_options)); + OrtEpContextConfig* write_config = nullptr; + ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(&internal_options->GetSessionOptions(), &write_config)); + auto release_write_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(write_config); }); + + ASSERT_ORTSTATUS_OK(ep_api.WriteEpContextData(write_config, "empty.bin", nullptr, nullptr, 0)); + EXPECT_TRUE(write_state.called); + EXPECT_EQ(write_state.file_name, "empty.bin"); + EXPECT_TRUE(write_state.payload.empty()); +#endif // !defined(ORT_MINIMAL_BUILD) +} + +TEST(PluginExecutionProviderTest, EpContextDataReadRejectsNonEmptyNullCallbackBuffer) { + const auto& ep_api = Ort::GetEpApi(); + Ort::SessionOptions session_options; + session_options.SetEpContextDataReadFunc(EpContextNonEmptyNullBufferReadCallback, nullptr); + + OrtEpContextConfig* ep_context_config = nullptr; + ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(session_options, &ep_context_config)); + auto release_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(ep_context_config); }); + + Ort::AllocatorWithDefaultOptions allocator; + void* buffer = nullptr; + size_t buffer_size = 0; + ExpectOrtStatus(ep_api.ReadEpContextData(ep_context_config, "context.bin", nullptr, allocator, + &buffer, &buffer_size), + ORT_FAIL, "returned a null buffer for non-empty EPContext data"); +} + #if !defined(ORT_MINIMAL_BUILD) TEST(PluginExecutionProviderTest, EpContextDataWriteFuncIsCalledViaEpApi) { const auto& ep_api = Ort::GetEpApi(); @@ -1850,6 +2036,66 @@ TEST(PluginExecutionProviderTest, EpContextDataFallsBackToDisk) { EXPECT_EQ(std::vector(static_cast(buffer), static_cast(buffer) + buffer_size), payload); } +TEST(PluginExecutionProviderTest, EpContextDataDiskFallbackResolvesRelativePathAgainstGraphModelPath) { + const auto& ep_api = Ort::GetEpApi(); + const std::filesystem::path test_dir = MakeEpContextDataTestDir("ort_ep_context_data_relative_path_test"); + auto cleanup = gsl::finally([&]() { + std::error_code ec; + std::filesystem::remove_all(test_dir, ec); + }); + + const std::filesystem::path source_model_path{ORT_TSTR("testdata/add_mul_add.onnx")}; + const std::filesystem::path model_path = test_dir / "model.onnx"; + std::filesystem::copy_file(source_model_path, model_path, std::filesystem::copy_options::overwrite_existing); + + std::shared_ptr model; + ASSERT_STATUS_OK(Model::Load(model_path.native().c_str(), model, nullptr, + DefaultLoggingManager().DefaultLogger())); + GraphViewer graph_viewer(model->MainGraph()); + std::unique_ptr ep_graph = nullptr; + ASSERT_STATUS_OK(EpGraph::Create(graph_viewer, ep_graph, true)); + + const std::vector payload{'r', 'e', 'l'}; + ASSERT_ORTSTATUS_OK(ep_api.WriteEpContextData(nullptr, "context.bin", ep_graph.get(), + payload.data(), payload.size())); + + const std::filesystem::path expected_context_path = test_dir / "context.bin"; + ASSERT_TRUE(std::filesystem::exists(expected_context_path)); + + Ort::AllocatorWithDefaultOptions allocator; + void* buffer = nullptr; + size_t buffer_size = 0; + ASSERT_ORTSTATUS_OK(ep_api.ReadEpContextData(nullptr, "context.bin", ep_graph.get(), allocator, + &buffer, &buffer_size)); + auto release_buffer = gsl::finally([&]() { allocator.Free(buffer); }); + + ASSERT_EQ(buffer_size, payload.size()); + EXPECT_EQ(std::vector(static_cast(buffer), static_cast(buffer) + buffer_size), payload); +} + +TEST(PluginExecutionProviderTest, EpContextDataDiskFallbackReportsFileErrors) { + const auto& ep_api = Ort::GetEpApi(); + const std::filesystem::path test_dir = MakeEpContextDataTestDir("ort_ep_context_data_file_error_test"); + auto cleanup = gsl::finally([&]() { + std::error_code ec; + std::filesystem::remove_all(test_dir, ec); + }); + + const std::filesystem::path missing_file_path = test_dir / "missing" / "context.bin"; + const std::string missing_file_path_utf8 = PathToUTF8String(missing_file_path.native()); + const std::vector payload{'x'}; + + ExpectOrtStatus(ep_api.WriteEpContextData(nullptr, missing_file_path_utf8.c_str(), nullptr, + payload.data(), payload.size()), + ORT_FAIL, "Failed to open EPContext data file for write"); + + Ort::AllocatorWithDefaultOptions allocator; + void* buffer = nullptr; + size_t buffer_size = 0; + ExpectOrtStatusNotOk(ep_api.ReadEpContextData(nullptr, missing_file_path_utf8.c_str(), nullptr, allocator, + &buffer, &buffer_size)); +} + // Helper: create a no-threshold resource accountant via the real factory (config ","). static IResourceAccountant* CreateNoThresholdAccountant(std::optional& acc_map) { ConfigOptions config; From 73b52ecdba150482c1f64c66f55d6987863abe77 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Fri, 15 May 2026 17:19:50 -0700 Subject: [PATCH 03/48] Document EPContext data callback contracts --- .../core/session/onnxruntime_c_api.h | 36 +++++++++++++++---- .../core/session/onnxruntime_ep_c_api.h | 19 ++++++++-- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/include/onnxruntime/core/session/onnxruntime_c_api.h b/include/onnxruntime/core/session/onnxruntime_c_api.h index 696bd57ab797a..4dcc260e9750a 100644 --- a/include/onnxruntime/core/session/onnxruntime_c_api.h +++ b/include/onnxruntime/core/session/onnxruntime_c_api.h @@ -605,11 +605,21 @@ typedef OrtStatus*(ORT_API_CALL* OrtWriteBufferFunc)(_In_ void* state, /** \brief Function called to write EPContext binary data during compilation. * - * This function may be called repeatedly with chunks until the entire EPContext binary for a given file_name has been - * written. The application's implementation can process the data in any way (e.g., encrypt and store, upload to cloud - * storage, or compress) before persisting it. + * This function is called synchronously by OrtEpApi::WriteEpContextData on the calling thread. ORT does not retain + * buffer after the callback returns, does not reorder callback invocations, and does not serialize invocations made by + * different EP instances or EP worker threads. * - * \param[in] state Opaque pointer holding the user's state. + * Each callback invocation represents one complete write operation for file_name. The callback signature does not + * provide an offset, sequence number, or final-chunk marker, so EPs that need chunked streaming must define their own + * ordering and completion contract with the application. EPs should prefer a single callback invocation per EPContext + * binary unless chunking semantics are documented by that EP. + * + * The application's implementation can process the data in any way (e.g., encrypt and store, upload to cloud storage, + * or compress) before persisting it. + * + * \param[in] state Opaque pointer holding the user's state. ORT does not own or manage this pointer. The application + * must keep it valid for the duration of any compile operation that may invoke this callback and must + * provide any synchronization required if it can be used concurrently. * \param[in] file_name The intended EPContext binary file name as a null-terminated UTF-8 string. * \param[in] buffer The buffer containing EPContext binary data to write. * \param[in] buffer_num_bytes The size of the buffer in bytes. @@ -626,9 +636,13 @@ typedef OrtStatus*(ORT_API_CALL* OrtWriteEpContextDataFunc)(_In_ void* state, /** \brief Function called by ORT to read EPContext binary data during session load. * * The application reads, processes (e.g., decrypts, decompresses, downloads), and returns the EPContext binary data. - * ORT provides an allocator so the application can allocate the output buffer directly. + * ORT provides an allocator so the application can allocate the output buffer directly. The callback is called + * synchronously by OrtEpApi::ReadEpContextData on the calling thread. ORT does not serialize invocations made by + * different EP instances or EP worker threads. * - * \param[in] state Opaque pointer holding the user's state. + * \param[in] state Opaque pointer holding the user's state. ORT does not own or manage this pointer. The application + * must keep it valid while any session or EP created from the associated OrtSessionOptions may invoke + * this callback and must provide any synchronization required if it can be used concurrently. * \param[in] file_name The EPContext binary file name as a null-terminated UTF-8 string. * \param[in] allocator ORT-provided allocator. The application must use this to allocate the output buffer. * \param[out] buffer Set by the implementation to the allocated buffer containing the output data. @@ -7557,6 +7571,10 @@ struct OrtApi { * * When loading a compiled model with external (non-embedded) EPContext binary data, an execution provider can use * OrtEpApi::ReadEpContextData to call this callback instead of reading the binary data from disk. + * + * The state pointer is stored as-is and is not owned by ORT. It must remain valid while any session or EP created + * from these options may call the callback. If the same state may be used by multiple EPs or threads, the application + * is responsible for synchronization. * * \param[in] options The OrtSessionOptions instance. * \param[in] read_func The OrtReadEpContextDataFunc callback. @@ -8415,9 +8433,13 @@ struct OrtCompileApi { * * When EPContext embed mode is disabled, execution providers can use OrtEpApi::WriteEpContextData to call this * callback instead of writing EPContext binary data directly to disk. + * + * The state pointer is stored as-is and is not owned by ORT. It must remain valid for the duration of the compile + * operation that may call the callback. If the same state may be used by multiple EPs or threads, the application is + * responsible for synchronization. * * \param[in] model_compile_options The OrtModelCompilationOptions instance. - * \param[in] write_func The OrtWriteEpContextDataFunc called to stream out EPContext bytes. + * \param[in] write_func The OrtWriteEpContextDataFunc called to write EPContext bytes. * \param[in] state Opaque state passed to write_func. Can be NULL. * * \snippet{doc} snippets.dox OrtStatus Return Value diff --git a/include/onnxruntime/core/session/onnxruntime_ep_c_api.h b/include/onnxruntime/core/session/onnxruntime_ep_c_api.h index 4f439c90cdded..6ccdb532b3605 100644 --- a/include/onnxruntime/core/session/onnxruntime_ep_c_api.h +++ b/include/onnxruntime/core/session/onnxruntime_ep_c_api.h @@ -2084,6 +2084,10 @@ struct OrtEpApi { * Extracts EPContext-related data I/O callbacks from the session options into an opaque OrtEpContextConfig handle. * The EP should call this during CreateEp() while session_options is still valid, and store the returned handle for * use during Compile(). The returned config is always non-NULL and must be released with ReleaseEpContextConfig. + * + * The returned handle owns only ORT's copy of callback function pointers and opaque state pointer values. It does not + * own the application-provided state. The application is responsible for keeping callback state valid and + * synchronized while an EP may call ReadEpContextData or WriteEpContextData with this config. * * \param[in] session_options The OrtSessionOptions instance. * \param[out] config The extracted OrtEpContextConfig. @@ -2108,10 +2112,13 @@ struct OrtEpApi { * * If config contains a read callback, the callback is invoked with the provided allocator. Otherwise, ORT reads the * file from disk. The disk fallback derives the base directory from the graph's model path. + * + * This function is synchronous. If a callback is present, it is invoked on the calling thread and its OrtStatus is + * returned to the caller. ORT does not serialize concurrent calls across EP instances or EP worker threads. * * \param[in] config The OrtEpContextConfig from SessionOptions_GetEpContextConfig. May be NULL for disk fallback. * \param[in] file_name EPContext file name as a null-terminated UTF-8 string. - * \param[in] graph The OrtGraph from which ORT derives the model path for disk fallback. May be NULL. + * \param[in] graph The OrtGraph from which ORT derives the model path for disk fallback. May be NULL. * \param[in] allocator Allocator for the output buffer. * \param[out] buffer Output buffer containing the EPContext binary data. * \param[out] buffer_size Size of the output buffer in bytes. @@ -2132,10 +2139,18 @@ struct OrtEpApi { * * If config contains a write callback, the data is forwarded to the application's callback. Otherwise, ORT writes the * data to disk. The disk fallback derives the base directory from the graph's model path. + * + * This function is synchronous. If a callback is present, it is invoked on the calling thread and its OrtStatus is + * returned to the caller. ORT does not retain buffer after the callback returns, reorder callback invocations, or + * serialize concurrent calls across EP instances or EP worker threads. + * + * Each call is one complete write operation for file_name. The API does not provide an offset, sequence number, or + * final-chunk marker. EPs should prefer one call per EPContext binary, or document EP-specific chunk ordering and + * completion semantics if multiple calls are made for the same file_name. * * \param[in] config The OrtEpContextConfig from SessionOptions_GetEpContextConfig. May be NULL for disk fallback. * \param[in] file_name EPContext file name as a null-terminated UTF-8 string. - * \param[in] graph The OrtGraph from which ORT derives the model path for disk fallback. May be NULL. + * \param[in] graph The OrtGraph from which ORT derives the model path for disk fallback. May be NULL. * \param[in] buffer The buffer containing EPContext binary data to write. * \param[in] buffer_size Size of the buffer in bytes. * From 7ee974e5815f4d72eb9727bc535a658ba9011b0a Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Mon, 18 May 2026 17:50:22 -0700 Subject: [PATCH 04/48] Apply lint formatting fixes --- .../core/session/onnxruntime_c_api.h | 18 +++++----- .../core/session/onnxruntime_cxx_inline.h | 2 +- .../core/session/onnxruntime_ep_c_api.h | 34 +++++++++---------- .../library/example_plugin_ep/ep_factory.cc | 2 +- .../test/framework/ep_plugin_provider_test.cc | 4 +-- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/include/onnxruntime/core/session/onnxruntime_c_api.h b/include/onnxruntime/core/session/onnxruntime_c_api.h index 4dcc260e9750a..441ac14b2f516 100644 --- a/include/onnxruntime/core/session/onnxruntime_c_api.h +++ b/include/onnxruntime/core/session/onnxruntime_c_api.h @@ -7571,10 +7571,10 @@ struct OrtApi { * * When loading a compiled model with external (non-embedded) EPContext binary data, an execution provider can use * OrtEpApi::ReadEpContextData to call this callback instead of reading the binary data from disk. - * - * The state pointer is stored as-is and is not owned by ORT. It must remain valid while any session or EP created - * from these options may call the callback. If the same state may be used by multiple EPs or threads, the application - * is responsible for synchronization. + * + * The state pointer is stored as-is and is not owned by ORT. It must remain valid while any session or EP created + * from these options may call the callback. If the same state may be used by multiple EPs or threads, the application + * is responsible for synchronization. * * \param[in] options The OrtSessionOptions instance. * \param[in] read_func The OrtReadEpContextDataFunc callback. @@ -8433,13 +8433,13 @@ struct OrtCompileApi { * * When EPContext embed mode is disabled, execution providers can use OrtEpApi::WriteEpContextData to call this * callback instead of writing EPContext binary data directly to disk. - * - * The state pointer is stored as-is and is not owned by ORT. It must remain valid for the duration of the compile - * operation that may call the callback. If the same state may be used by multiple EPs or threads, the application is - * responsible for synchronization. + * + * The state pointer is stored as-is and is not owned by ORT. It must remain valid for the duration of the compile + * operation that may call the callback. If the same state may be used by multiple EPs or threads, the application is + * responsible for synchronization. * * \param[in] model_compile_options The OrtModelCompilationOptions instance. - * \param[in] write_func The OrtWriteEpContextDataFunc called to write EPContext bytes. + * \param[in] write_func The OrtWriteEpContextDataFunc called to write EPContext bytes. * \param[in] state Opaque state passed to write_func. Can be NULL. * * \snippet{doc} snippets.dox OrtStatus Return Value diff --git a/include/onnxruntime/core/session/onnxruntime_cxx_inline.h b/include/onnxruntime/core/session/onnxruntime_cxx_inline.h index 303de4f635461..51acec9ba75e7 100644 --- a/include/onnxruntime/core/session/onnxruntime_cxx_inline.h +++ b/include/onnxruntime/core/session/onnxruntime_cxx_inline.h @@ -1582,7 +1582,7 @@ inline SessionOptionsImpl& SessionOptionsImpl::AddExternalInitializersFrom template inline SessionOptionsImpl& SessionOptionsImpl::SetEpContextDataReadFunc(OrtReadEpContextDataFunc read_func, - void* state) { + void* state) { ThrowOnError(GetApi().SessionOptions_SetEpContextDataReadFunc(this->p_, read_func, state)); return *this; } diff --git a/include/onnxruntime/core/session/onnxruntime_ep_c_api.h b/include/onnxruntime/core/session/onnxruntime_ep_c_api.h index 6ccdb532b3605..cf0b8e4743f81 100644 --- a/include/onnxruntime/core/session/onnxruntime_ep_c_api.h +++ b/include/onnxruntime/core/session/onnxruntime_ep_c_api.h @@ -2084,10 +2084,10 @@ struct OrtEpApi { * Extracts EPContext-related data I/O callbacks from the session options into an opaque OrtEpContextConfig handle. * The EP should call this during CreateEp() while session_options is still valid, and store the returned handle for * use during Compile(). The returned config is always non-NULL and must be released with ReleaseEpContextConfig. - * - * The returned handle owns only ORT's copy of callback function pointers and opaque state pointer values. It does not - * own the application-provided state. The application is responsible for keeping callback state valid and - * synchronized while an EP may call ReadEpContextData or WriteEpContextData with this config. + * + * The returned handle owns only ORT's copy of callback function pointers and opaque state pointer values. It does not + * own the application-provided state. The application is responsible for keeping callback state valid and + * synchronized while an EP may call ReadEpContextData or WriteEpContextData with this config. * * \param[in] session_options The OrtSessionOptions instance. * \param[out] config The extracted OrtEpContextConfig. @@ -2112,13 +2112,13 @@ struct OrtEpApi { * * If config contains a read callback, the callback is invoked with the provided allocator. Otherwise, ORT reads the * file from disk. The disk fallback derives the base directory from the graph's model path. - * - * This function is synchronous. If a callback is present, it is invoked on the calling thread and its OrtStatus is - * returned to the caller. ORT does not serialize concurrent calls across EP instances or EP worker threads. + * + * This function is synchronous. If a callback is present, it is invoked on the calling thread and its OrtStatus is + * returned to the caller. ORT does not serialize concurrent calls across EP instances or EP worker threads. * * \param[in] config The OrtEpContextConfig from SessionOptions_GetEpContextConfig. May be NULL for disk fallback. * \param[in] file_name EPContext file name as a null-terminated UTF-8 string. - * \param[in] graph The OrtGraph from which ORT derives the model path for disk fallback. May be NULL. + * \param[in] graph The OrtGraph from which ORT derives the model path for disk fallback. May be NULL. * \param[in] allocator Allocator for the output buffer. * \param[out] buffer Output buffer containing the EPContext binary data. * \param[out] buffer_size Size of the output buffer in bytes. @@ -2139,18 +2139,18 @@ struct OrtEpApi { * * If config contains a write callback, the data is forwarded to the application's callback. Otherwise, ORT writes the * data to disk. The disk fallback derives the base directory from the graph's model path. - * - * This function is synchronous. If a callback is present, it is invoked on the calling thread and its OrtStatus is - * returned to the caller. ORT does not retain buffer after the callback returns, reorder callback invocations, or - * serialize concurrent calls across EP instances or EP worker threads. - * - * Each call is one complete write operation for file_name. The API does not provide an offset, sequence number, or - * final-chunk marker. EPs should prefer one call per EPContext binary, or document EP-specific chunk ordering and - * completion semantics if multiple calls are made for the same file_name. + * + * This function is synchronous. If a callback is present, it is invoked on the calling thread and its OrtStatus is + * returned to the caller. ORT does not retain buffer after the callback returns, reorder callback invocations, or + * serialize concurrent calls across EP instances or EP worker threads. + * + * Each call is one complete write operation for file_name. The API does not provide an offset, sequence number, or + * final-chunk marker. EPs should prefer one call per EPContext binary, or document EP-specific chunk ordering and + * completion semantics if multiple calls are made for the same file_name. * * \param[in] config The OrtEpContextConfig from SessionOptions_GetEpContextConfig. May be NULL for disk fallback. * \param[in] file_name EPContext file name as a null-terminated UTF-8 string. - * \param[in] graph The OrtGraph from which ORT derives the model path for disk fallback. May be NULL. + * \param[in] graph The OrtGraph from which ORT derives the model path for disk fallback. May be NULL. * \param[in] buffer The buffer containing EPContext binary data to write. * \param[in] buffer_size Size of the buffer in bytes. * diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc index 1d8f3e4a11fe9..668be73c31f3b 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc @@ -243,7 +243,7 @@ OrtStatus* ORT_API_CALL ExampleEpFactory::CreateEpImpl(OrtEpFactory* this_ptr, ep_context_config.reset(ep_context_config_raw); auto dummy_ep = std::make_unique(*factory, factory->ep_name_, config, *logger, - ep_context_config.release()); + ep_context_config.release()); *ep = dummy_ep.release(); return nullptr; diff --git a/onnxruntime/test/framework/ep_plugin_provider_test.cc b/onnxruntime/test/framework/ep_plugin_provider_test.cc index 1cf183873eaae..a6a0b01261433 100644 --- a/onnxruntime/test/framework/ep_plugin_provider_test.cc +++ b/onnxruntime/test/framework/ep_plugin_provider_test.cc @@ -1881,10 +1881,10 @@ TEST(PluginExecutionProviderTest, EpContextDataApiRejectsInvalidArguments) { Ort::ModelCompilationOptions compilation_options{env, session_options}; const auto& compile_api = Ort::GetCompileApi(); ExpectOrtStatus(compile_api.ModelCompilationOptions_SetEpContextDataWriteFunc(nullptr, EpContextWriteCallback, - nullptr), + nullptr), ORT_INVALID_ARGUMENT, "OrtModelCompilationOptions is NULL"); ExpectOrtStatus(compile_api.ModelCompilationOptions_SetEpContextDataWriteFunc(compilation_options, nullptr, - nullptr), + nullptr), ORT_INVALID_ARGUMENT, "OrtWriteEpContextDataFunc function is null"); #endif // !defined(ORT_MINIMAL_BUILD) } From 18f515f78bfd8d24bd03cd0974ddd4b5b1e25055 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Mon, 18 May 2026 18:04:20 -0700 Subject: [PATCH 05/48] Fix EPContext config release API docs --- include/onnxruntime/core/session/onnxruntime_ep_c_api.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/onnxruntime/core/session/onnxruntime_ep_c_api.h b/include/onnxruntime/core/session/onnxruntime_ep_c_api.h index cf0b8e4743f81..5370a8213c2ac 100644 --- a/include/onnxruntime/core/session/onnxruntime_ep_c_api.h +++ b/include/onnxruntime/core/session/onnxruntime_ep_c_api.h @@ -2102,7 +2102,7 @@ struct OrtEpApi { /** \brief Release an OrtEpContextConfig instance. * - * \param[in] config The OrtEpContextConfig instance to release. May be NULL. + * \param[in] input The OrtEpContextConfig instance to release. May be NULL. * * \since Version 1.27. */ From c02af097e9a4a170ebc7e6492ac8513716c8b93e Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Thu, 21 May 2026 17:26:34 -0700 Subject: [PATCH 06/48] Refactor EPContext data callbacks for EP-owned I/O --- .../core/session/onnxruntime_c_api.h | 22 +- .../core/session/onnxruntime_cxx_api.h | 17 + .../core/session/onnxruntime_cxx_inline.h | 18 + .../core/session/onnxruntime_ep_c_api.h | 75 ++-- onnxruntime/core/session/compile_api.cc | 5 + onnxruntime/core/session/plugin_ep/ep_api.cc | 120 ++----- onnxruntime/core/session/plugin_ep/ep_api.h | 21 +- .../autoep/library/example_plugin_ep/ep.cc | 189 ++++++++++- .../autoep/library/example_plugin_ep/ep.h | 4 +- .../library/example_plugin_ep/ep_factory.cc | 10 +- .../test/framework/ep_plugin_provider_test.cc | 320 +++++------------- 11 files changed, 374 insertions(+), 427 deletions(-) diff --git a/include/onnxruntime/core/session/onnxruntime_c_api.h b/include/onnxruntime/core/session/onnxruntime_c_api.h index 441ac14b2f516..20d604c6fc1f7 100644 --- a/include/onnxruntime/core/session/onnxruntime_c_api.h +++ b/include/onnxruntime/core/session/onnxruntime_c_api.h @@ -605,9 +605,9 @@ typedef OrtStatus*(ORT_API_CALL* OrtWriteBufferFunc)(_In_ void* state, /** \brief Function called to write EPContext binary data during compilation. * - * This function is called synchronously by OrtEpApi::WriteEpContextData on the calling thread. ORT does not retain - * buffer after the callback returns, does not reorder callback invocations, and does not serialize invocations made by - * different EP instances or EP worker threads. + * This function is called synchronously by the execution provider on the calling thread. ORT does not own or retain + * buffer after the callback returns. ORT does not serialize invocations made by different EP instances or EP worker + * threads. * * Each callback invocation represents one complete write operation for file_name. The callback signature does not * provide an offset, sequence number, or final-chunk marker, so EPs that need chunked streaming must define their own @@ -637,8 +637,8 @@ typedef OrtStatus*(ORT_API_CALL* OrtWriteEpContextDataFunc)(_In_ void* state, * * The application reads, processes (e.g., decrypts, decompresses, downloads), and returns the EPContext binary data. * ORT provides an allocator so the application can allocate the output buffer directly. The callback is called - * synchronously by OrtEpApi::ReadEpContextData on the calling thread. ORT does not serialize invocations made by - * different EP instances or EP worker threads. + * synchronously by the execution provider on the calling thread. ORT does not serialize invocations made by different + * EP instances or EP worker threads. * * \param[in] state Opaque pointer holding the user's state. ORT does not own or manage this pointer. The application * must keep it valid while any session or EP created from the associated OrtSessionOptions may invoke @@ -7569,8 +7569,8 @@ struct OrtApi { /** \brief Registers a callback to provide EPContext binary data during session load. * - * When loading a compiled model with external (non-embedded) EPContext binary data, an execution provider can use - * OrtEpApi::ReadEpContextData to call this callback instead of reading the binary data from disk. + * When loading a compiled model with external (non-embedded) EPContext binary data, an execution provider can + * retrieve this callback from OrtEpContextConfig and call it instead of reading the binary data from disk. * * The state pointer is stored as-is and is not owned by ORT. It must remain valid while any session or EP created * from these options may call the callback. If the same state may be used by multiple EPs or threads, the application @@ -7582,7 +7582,7 @@ struct OrtApi { * * \snippet{doc} snippets.dox OrtStatus Return Value * - * \since Version 1.27. + * \since Version 1.28. */ ORT_API2_STATUS(SessionOptions_SetEpContextDataReadFunc, _Inout_ OrtSessionOptions* options, _In_ OrtReadEpContextDataFunc read_func, _In_opt_ void* state); @@ -8431,8 +8431,8 @@ struct OrtCompileApi { /** \brief Sets a callback for writing EPContext binary data during compilation. * - * When EPContext embed mode is disabled, execution providers can use OrtEpApi::WriteEpContextData to call this - * callback instead of writing EPContext binary data directly to disk. + * When EPContext embed mode is disabled, execution providers can retrieve this callback from OrtEpContextConfig and + * call it instead of writing EPContext binary data directly to disk. * * The state pointer is stored as-is and is not owned by ORT. It must remain valid for the duration of the compile * operation that may call the callback. If the same state may be used by multiple EPs or threads, the application is @@ -8444,7 +8444,7 @@ struct OrtCompileApi { * * \snippet{doc} snippets.dox OrtStatus Return Value * - * \since Version 1.27. + * \since Version 1.28. */ ORT_API2_STATUS(ModelCompilationOptions_SetEpContextDataWriteFunc, _In_ OrtModelCompilationOptions* model_compile_options, diff --git a/include/onnxruntime/core/session/onnxruntime_cxx_api.h b/include/onnxruntime/core/session/onnxruntime_cxx_api.h index 3b5d38593bc3f..c8b3cb200552d 100644 --- a/include/onnxruntime/core/session/onnxruntime_cxx_api.h +++ b/include/onnxruntime/core/session/onnxruntime_cxx_api.h @@ -658,6 +658,7 @@ ORT_DEFINE_RELEASE(Value); ORT_DEFINE_RELEASE(ValueInfo); ORT_DEFINE_RELEASE_FROM_API_STRUCT(ModelCompilationOptions, GetCompileApi); +ORT_DEFINE_RELEASE_FROM_API_STRUCT(EpContextConfig, GetEpApi); ORT_DEFINE_RELEASE_FROM_API_STRUCT(EpDevice, GetEpApi); ORT_DEFINE_RELEASE_FROM_API_STRUCT(KernelDef, GetEpApi); ORT_DEFINE_RELEASE_FROM_API_STRUCT(KernelDefBuilder, GetEpApi); @@ -786,6 +787,7 @@ struct AllocatedFree { struct AllocatorWithDefaultOptions; struct Env; +struct EpContextConfig; struct EpDevice; struct ExternalInitializerInfo; struct Graph; @@ -1185,6 +1187,21 @@ struct EpDevice : detail::EpDeviceImpl { ConstKeyValuePairs ep_metadata = {}, ConstKeyValuePairs ep_options = {}); }; +/** \brief Owning wrapper around ::OrtEpContextConfig. */ +struct EpContextConfig : detail::Base { + explicit EpContextConfig(std::nullptr_t) {} ///< No instance is created + explicit EpContextConfig(OrtEpContextConfig* p) : Base{p} {} ///< Take ownership + + /// \brief Wraps OrtEpApi::SessionOptions_GetEpContextConfig + explicit EpContextConfig(const OrtSessionOptions* session_options); + + /// \brief Wraps OrtEpApi::EpContextConfig_GetEpContextDataReadFunc + std::pair GetEpContextDataReadFunc() const; + + /// \brief Wraps OrtEpApi::EpContextConfig_GetEpContextDataWriteFunc + std::pair GetEpContextDataWriteFunc() const; +}; + /** \brief Validate a compiled model's compatibility for one or more EP devices. * * Throws on error. Returns the resulting compatibility status. diff --git a/include/onnxruntime/core/session/onnxruntime_cxx_inline.h b/include/onnxruntime/core/session/onnxruntime_cxx_inline.h index 51acec9ba75e7..3eb19179002e1 100644 --- a/include/onnxruntime/core/session/onnxruntime_cxx_inline.h +++ b/include/onnxruntime/core/session/onnxruntime_cxx_inline.h @@ -769,6 +769,24 @@ inline EpDevice::EpDevice(OrtEpFactory& ep_factory, ConstHardwareDevice& hardwar ThrowOnError(GetEpApi().CreateEpDevice(&ep_factory, hardware_device, ep_metadata, ep_options, &p_)); } +inline EpContextConfig::EpContextConfig(const OrtSessionOptions* session_options) { + ThrowOnError(GetEpApi().SessionOptions_GetEpContextConfig(session_options, &p_)); +} + +inline std::pair EpContextConfig::GetEpContextDataReadFunc() const { + OrtReadEpContextDataFunc read_func = nullptr; + void* state = nullptr; + ThrowOnError(GetEpApi().EpContextConfig_GetEpContextDataReadFunc(this->p_, &read_func, &state)); + return {read_func, state}; +} + +inline std::pair EpContextConfig::GetEpContextDataWriteFunc() const { + OrtWriteEpContextDataFunc write_func = nullptr; + void* state = nullptr; + ThrowOnError(GetEpApi().EpContextConfig_GetEpContextDataWriteFunc(this->p_, &write_func, &state)); + return {write_func, state}; +} + namespace detail { template inline std::string EpAssignedSubgraphImpl::GetEpName() const { diff --git a/include/onnxruntime/core/session/onnxruntime_ep_c_api.h b/include/onnxruntime/core/session/onnxruntime_ep_c_api.h index 5370a8213c2ac..08ecc5b70a97f 100644 --- a/include/onnxruntime/core/session/onnxruntime_ep_c_api.h +++ b/include/onnxruntime/core/session/onnxruntime_ep_c_api.h @@ -2087,14 +2087,14 @@ struct OrtEpApi { * * The returned handle owns only ORT's copy of callback function pointers and opaque state pointer values. It does not * own the application-provided state. The application is responsible for keeping callback state valid and - * synchronized while an EP may call ReadEpContextData or WriteEpContextData with this config. + * synchronized while an EP may call callbacks retrieved from this config. * * \param[in] session_options The OrtSessionOptions instance. * \param[out] config The extracted OrtEpContextConfig. * * \snippet{doc} snippets.dox OrtStatus Return Value * - * \since Version 1.27. + * \since Version 1.28. */ ORT_API2_STATUS(SessionOptions_GetEpContextConfig, _In_ const OrtSessionOptions* session_options, @@ -2104,66 +2104,49 @@ struct OrtEpApi { * * \param[in] input The OrtEpContextConfig instance to release. May be NULL. * - * \since Version 1.27. + * \since Version 1.28. */ ORT_CLASS_RELEASE(EpContextConfig); - /** \brief Read EPContext binary data, using an application read callback or falling back to disk. + /** \brief Get the application-provided EPContext data read callback. * - * If config contains a read callback, the callback is invoked with the provided allocator. Otherwise, ORT reads the - * file from disk. The disk fallback derives the base directory from the graph's model path. + * Returns the OrtReadEpContextDataFunc and opaque state pointer registered via + * OrtApi::SessionOptions_SetEpContextDataReadFunc. If no callback was registered, *read_func and *state are set to + * NULL. The EP is responsible for calling the callback when present and for using its own normal read path when no + * callback is present. * - * This function is synchronous. If a callback is present, it is invoked on the calling thread and its OrtStatus is - * returned to the caller. ORT does not serialize concurrent calls across EP instances or EP worker threads. - * - * \param[in] config The OrtEpContextConfig from SessionOptions_GetEpContextConfig. May be NULL for disk fallback. - * \param[in] file_name EPContext file name as a null-terminated UTF-8 string. - * \param[in] graph The OrtGraph from which ORT derives the model path for disk fallback. May be NULL. - * \param[in] allocator Allocator for the output buffer. - * \param[out] buffer Output buffer containing the EPContext binary data. - * \param[out] buffer_size Size of the output buffer in bytes. + * \param[in] config The OrtEpContextConfig from SessionOptions_GetEpContextConfig. + * \param[out] read_func The registered read callback, or NULL if none was registered. + * \param[out] state Opaque state pointer passed to read_func, or NULL if none was registered. * * \snippet{doc} snippets.dox OrtStatus Return Value * - * \since Version 1.27. + * \since Version 1.28. */ - ORT_API2_STATUS(ReadEpContextData, - _In_opt_ const OrtEpContextConfig* config, - _In_ const char* file_name, - _In_opt_ const OrtGraph* graph, - _Inout_ OrtAllocator* allocator, - _Outptr_ void** buffer, - _Out_ size_t* buffer_size); + ORT_API2_STATUS(EpContextConfig_GetEpContextDataReadFunc, + _In_ const OrtEpContextConfig* config, + _Out_ OrtReadEpContextDataFunc* read_func, + _Out_ void** state); - /** \brief Write EPContext binary data, using an application write callback or falling back to disk. + /** \brief Get the application-provided EPContext data write callback. * - * If config contains a write callback, the data is forwarded to the application's callback. Otherwise, ORT writes the - * data to disk. The disk fallback derives the base directory from the graph's model path. + * Returns the OrtWriteEpContextDataFunc and opaque state pointer registered via + * OrtCompileApi::ModelCompilationOptions_SetEpContextDataWriteFunc. If no callback was registered, *write_func and + * *state are set to NULL. The EP is responsible for calling the callback when present and for using its own normal + * write path when no callback is present. * - * This function is synchronous. If a callback is present, it is invoked on the calling thread and its OrtStatus is - * returned to the caller. ORT does not retain buffer after the callback returns, reorder callback invocations, or - * serialize concurrent calls across EP instances or EP worker threads. - * - * Each call is one complete write operation for file_name. The API does not provide an offset, sequence number, or - * final-chunk marker. EPs should prefer one call per EPContext binary, or document EP-specific chunk ordering and - * completion semantics if multiple calls are made for the same file_name. - * - * \param[in] config The OrtEpContextConfig from SessionOptions_GetEpContextConfig. May be NULL for disk fallback. - * \param[in] file_name EPContext file name as a null-terminated UTF-8 string. - * \param[in] graph The OrtGraph from which ORT derives the model path for disk fallback. May be NULL. - * \param[in] buffer The buffer containing EPContext binary data to write. - * \param[in] buffer_size Size of the buffer in bytes. + * \param[in] config The OrtEpContextConfig from SessionOptions_GetEpContextConfig. + * \param[out] write_func The registered write callback, or NULL if none was registered. + * \param[out] state Opaque state pointer passed to write_func, or NULL if none was registered. * * \snippet{doc} snippets.dox OrtStatus Return Value * - * \since Version 1.27. + * \since Version 1.28. */ - ORT_API2_STATUS(WriteEpContextData, - _In_opt_ const OrtEpContextConfig* config, - _In_ const char* file_name, - _In_opt_ const OrtGraph* graph, - _In_ const void* buffer, - _In_ size_t buffer_size); + ORT_API2_STATUS(EpContextConfig_GetEpContextDataWriteFunc, + _In_ const OrtEpContextConfig* config, + _Out_ OrtWriteEpContextDataFunc* write_func, + _Out_ void** state); }; /** diff --git a/onnxruntime/core/session/compile_api.cc b/onnxruntime/core/session/compile_api.cc index cdcbe7f3e867b..901f1754784a5 100644 --- a/onnxruntime/core/session/compile_api.cc +++ b/onnxruntime/core/session/compile_api.cc @@ -393,6 +393,9 @@ static constexpr OrtCompileApi ort_compile_api = { &OrtCompileAPI::ModelCompilationOptions_SetInputModel, // End of Version 24 - DO NOT MODIFY ABOVE + // End of Version 25 - DO NOT MODIFY ABOVE + // End of Version 26 - DO NOT MODIFY ABOVE + // End of Version 27 - DO NOT MODIFY ABOVE &OrtCompileAPI::ModelCompilationOptions_SetEpContextDataWriteFunc, }; @@ -404,6 +407,8 @@ static_assert(offsetof(OrtCompileApi, ModelCompilationOptions_SetOutputModelGetI "Size of version 23 of Api cannot change"); static_assert(offsetof(OrtCompileApi, ModelCompilationOptions_SetInputModel) / sizeof(void*) == 14, "Size of version 24 of Api cannot change"); +static_assert(offsetof(OrtCompileApi, ModelCompilationOptions_SetInputModel) / sizeof(void*) == 14, + "Size of version 27 of Api cannot change"); ORT_API(const OrtCompileApi*, OrtCompileAPI::GetCompileApi) { return &ort_compile_api; diff --git a/onnxruntime/core/session/plugin_ep/ep_api.cc b/onnxruntime/core/session/plugin_ep/ep_api.cc index 45c0bf9aabd8b..0ecdb2579eb18 100644 --- a/onnxruntime/core/session/plugin_ep/ep_api.cc +++ b/onnxruntime/core/session/plugin_ep/ep_api.cc @@ -5,15 +5,11 @@ #include #include -#include -#include -#include #include #include #include #include -#include "core/common/path_string.h" #include "core/common/semver.h" #include "core/framework/error_code_helper.h" #include "core/framework/func_api.h" @@ -51,25 +47,6 @@ struct OrtEpContextConfig { namespace OrtExecutionProviderApi { -namespace { - -std::filesystem::path ResolveEpContextDataPath(const char* file_name, const OrtGraph* graph) { - std::filesystem::path data_path{ToPathString(file_name)}; - if (data_path.is_absolute() || graph == nullptr) { - return data_path; - } - - const ORTCHAR_T* model_path = graph->GetModelPath(); - if (model_path == nullptr || model_path[0] == 0) { - return data_path; - } - - std::filesystem::path model_file_path{model_path}; - return model_file_path.parent_path() / data_path; -} - -} // namespace - ORT_API_STATUS_IMPL(CreateEpDevice, _In_ OrtEpFactory* ep_factory, _In_ const OrtHardwareDevice* hardware_device, _In_opt_ const OrtKeyValuePairs* ep_metadata, @@ -1255,85 +1232,32 @@ ORT_API(void, ReleaseEpContextConfig, _Frees_ptr_opt_ OrtEpContextConfig* config delete config; } -ORT_API_STATUS_IMPL(ReadEpContextData, - _In_opt_ const OrtEpContextConfig* config, - _In_ const char* file_name, - _In_opt_ const OrtGraph* graph, - _Inout_ OrtAllocator* allocator, - _Outptr_ void** buffer, - _Out_ size_t* buffer_size) { +ORT_API_STATUS_IMPL(EpContextConfig_GetEpContextDataReadFunc, + _In_ const OrtEpContextConfig* config, + _Out_ OrtReadEpContextDataFunc* read_func, + _Out_ void** state) { API_IMPL_BEGIN - ORT_API_RETURN_IF(file_name == nullptr, ORT_INVALID_ARGUMENT, "file_name is NULL"); - ORT_API_RETURN_IF(allocator == nullptr, ORT_INVALID_ARGUMENT, "OrtAllocator is NULL"); - ORT_API_RETURN_IF(buffer == nullptr, ORT_INVALID_ARGUMENT, "Output buffer is NULL"); - ORT_API_RETURN_IF(buffer_size == nullptr, ORT_INVALID_ARGUMENT, "Output buffer_size is NULL"); - - *buffer = nullptr; - *buffer_size = 0; - - if (config != nullptr && config->read_func != nullptr) { - OrtStatus* status = config->read_func(config->read_state, file_name, allocator, buffer, buffer_size); - if (status != nullptr) { - return status; - } - - ORT_API_RETURN_IF(*buffer_size != 0 && *buffer == nullptr, ORT_FAIL, - "OrtReadEpContextDataFunc returned a null buffer for non-empty EPContext data"); - return nullptr; - } - - const std::filesystem::path data_path = ResolveEpContextDataPath(file_name, graph); - size_t file_size = 0; - ORT_API_RETURN_IF_STATUS_NOT_OK(Env::Default().GetFileLength(data_path.native().c_str(), file_size)); + ORT_API_RETURN_IF(config == nullptr, ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); + ORT_API_RETURN_IF(read_func == nullptr, ORT_INVALID_ARGUMENT, "Output read_func is NULL"); + ORT_API_RETURN_IF(state == nullptr, ORT_INVALID_ARGUMENT, "Output state is NULL"); - if (file_size == 0) { - return nullptr; - } - - void* allocated_buffer = allocator->Alloc(allocator, file_size); - ORT_API_RETURN_IF(allocated_buffer == nullptr, ORT_FAIL, "Failed to allocate buffer for EPContext data"); - - const auto read_status = Env::Default().ReadFileIntoBuffer( - data_path.native().c_str(), 0, file_size, gsl::make_span(static_cast(allocated_buffer), file_size)); - if (!read_status.IsOK()) { - allocator->Free(allocator, allocated_buffer); - ORT_API_RETURN_IF_STATUS_NOT_OK(read_status); - } - - *buffer = allocated_buffer; - *buffer_size = file_size; + *read_func = config->read_func; + *state = config->read_func != nullptr ? config->read_state : nullptr; return nullptr; API_IMPL_END } -ORT_API_STATUS_IMPL(WriteEpContextData, - _In_opt_ const OrtEpContextConfig* config, - _In_ const char* file_name, - _In_opt_ const OrtGraph* graph, - _In_ const void* buffer, - _In_ size_t buffer_size) { +ORT_API_STATUS_IMPL(EpContextConfig_GetEpContextDataWriteFunc, + _In_ const OrtEpContextConfig* config, + _Out_ OrtWriteEpContextDataFunc* write_func, + _Out_ void** state) { API_IMPL_BEGIN - ORT_API_RETURN_IF(file_name == nullptr, ORT_INVALID_ARGUMENT, "file_name is NULL"); - ORT_API_RETURN_IF(buffer_size != 0 && buffer == nullptr, ORT_INVALID_ARGUMENT, - "EPContext data buffer is NULL for non-empty data"); - - if (config != nullptr && config->write_func != nullptr) { - return config->write_func(config->write_state, file_name, buffer, buffer_size); - } - - const std::filesystem::path data_path = ResolveEpContextDataPath(file_name, graph); - std::ofstream output_stream(data_path, std::ios::binary); - ORT_API_RETURN_IF(!output_stream, ORT_FAIL, "Failed to open EPContext data file for write: ", - PathToUTF8String(data_path.native())); - - if (buffer_size != 0) { - ORT_API_RETURN_IF(buffer_size > static_cast(std::numeric_limits::max()), - ORT_INVALID_ARGUMENT, "EPContext data buffer is too large to write"); - output_stream.write(static_cast(buffer), static_cast(buffer_size)); - ORT_API_RETURN_IF(!output_stream, ORT_FAIL, "Failed to write EPContext data file: ", - PathToUTF8String(data_path.native())); - } + ORT_API_RETURN_IF(config == nullptr, ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); + ORT_API_RETURN_IF(write_func == nullptr, ORT_INVALID_ARGUMENT, "Output write_func is NULL"); + ORT_API_RETURN_IF(state == nullptr, ORT_INVALID_ARGUMENT, "Output state is NULL"); + *write_func = config->write_func; + *state = config->write_func != nullptr ? config->write_state : nullptr; return nullptr; API_IMPL_END } @@ -1427,11 +1351,13 @@ static constexpr OrtEpApi ort_ep_api = { &OrtExecutionProviderApi::ProfilingEvent_GetArgValue, &OrtExecutionProviderApi::ProfilingEventsContainer_AddEvents, // End of Version 25 - DO NOT MODIFY ABOVE + // End of Version 26 - DO NOT MODIFY ABOVE + // End of Version 27 - DO NOT MODIFY ABOVE &OrtExecutionProviderApi::SessionOptions_GetEpContextConfig, &OrtExecutionProviderApi::ReleaseEpContextConfig, - &OrtExecutionProviderApi::ReadEpContextData, - &OrtExecutionProviderApi::WriteEpContextData, + &OrtExecutionProviderApi::EpContextConfig_GetEpContextDataReadFunc, + &OrtExecutionProviderApi::EpContextConfig_GetEpContextDataWriteFunc, }; // checks that we don't violate the rule that the functions must remain in the slots they were originally assigned @@ -1443,6 +1369,8 @@ static_assert(offsetof(OrtEpApi, GetEnvConfigEntries) / sizeof(void*) == 49, "Size of version 24 API cannot change"); static_assert(offsetof(OrtEpApi, ProfilingEventsContainer_AddEvents) / sizeof(void*) == 72, "Size of version 25 API cannot change"); +static_assert(offsetof(OrtEpApi, ProfilingEventsContainer_AddEvents) / sizeof(void*) == 72, + "Size of version 27 API cannot change"); } // namespace OrtExecutionProviderApi diff --git a/onnxruntime/core/session/plugin_ep/ep_api.h b/onnxruntime/core/session/plugin_ep/ep_api.h index 8ca542b59a425..e3b5903f5f121 100644 --- a/onnxruntime/core/session/plugin_ep/ep_api.h +++ b/onnxruntime/core/session/plugin_ep/ep_api.h @@ -184,18 +184,13 @@ ORT_API_STATUS_IMPL(SessionOptions_GetEpContextConfig, _In_ const OrtSessionOptions* session_options, _Outptr_ OrtEpContextConfig** config); ORT_API(void, ReleaseEpContextConfig, _Frees_ptr_opt_ OrtEpContextConfig* config); -ORT_API_STATUS_IMPL(ReadEpContextData, - _In_opt_ const OrtEpContextConfig* config, - _In_ const char* file_name, - _In_opt_ const OrtGraph* graph, - _Inout_ OrtAllocator* allocator, - _Outptr_ void** buffer, - _Out_ size_t* buffer_size); -ORT_API_STATUS_IMPL(WriteEpContextData, - _In_opt_ const OrtEpContextConfig* config, - _In_ const char* file_name, - _In_opt_ const OrtGraph* graph, - _In_ const void* buffer, - _In_ size_t buffer_size); +ORT_API_STATUS_IMPL(EpContextConfig_GetEpContextDataReadFunc, + _In_ const OrtEpContextConfig* config, + _Out_ OrtReadEpContextDataFunc* read_func, + _Out_ void** state); +ORT_API_STATUS_IMPL(EpContextConfig_GetEpContextDataWriteFunc, + _In_ const OrtEpContextConfig* config, + _Out_ OrtWriteEpContextDataFunc* write_func, + _Out_ void** state); } // namespace OrtExecutionProviderApi diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc index 7a05b921230bd..3f4a25a8a7248 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc @@ -4,20 +4,150 @@ #include "ep.h" #include +#include #include #include +#include +#include #include +#include +#include #include #include +#include #include #include -#include + +#ifdef _WIN32 +#include +#endif #include "ep_factory.h" #include "ep_stream_support.h" extern std::atomic g_sync_count; +namespace { + +#ifdef _WIN32 +std::wstring Utf8ToWideString(std::string_view value) { + if (value.empty() || value.size() > static_cast(std::numeric_limits::max())) { + return {}; + } + + const int wide_length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), + static_cast(value.size()), nullptr, 0); + if (wide_length <= 0) { + return {}; + } + + std::wstring wide_value(static_cast(wide_length), L'\0'); + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), static_cast(value.size()), + wide_value.data(), wide_length); + return wide_value; +} + +std::string WideToUtf8String(std::wstring_view value) { + if (value.empty() || value.size() > static_cast(std::numeric_limits::max())) { + return {}; + } + + const int utf8_length = WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast(value.size()), + nullptr, 0, nullptr, nullptr); + if (utf8_length <= 0) { + return {}; + } + + std::string utf8_value(static_cast(utf8_length), '\0'); + WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast(value.size()), utf8_value.data(), utf8_length, + nullptr, nullptr); + return utf8_value; +} +#endif + +std::filesystem::path Utf8Path(const char* path) { +#ifdef _WIN32 + return std::filesystem::path{Utf8ToWideString(path)}; +#else + return std::filesystem::path{path}; +#endif +} + +std::string PathToUtf8String(const std::filesystem::path& path) { +#ifdef _WIN32 + return WideToUtf8String(path.wstring()); +#else + return path.string(); +#endif +} + +OrtStatus* ResolveEpContextDataPath(const OrtApi& api, const char* file_name, const OrtGraph* graph, + std::filesystem::path& data_path) { + data_path = Utf8Path(file_name); + if (data_path.is_absolute() || graph == nullptr) { + return nullptr; + } + + const ORTCHAR_T* model_path = nullptr; + RETURN_IF_ERROR(api.Graph_GetModelPath(graph, &model_path)); + if (model_path == nullptr || model_path[0] == 0) { + return nullptr; + } + + data_path = std::filesystem::path{model_path}.parent_path() / data_path; + return nullptr; +} + +OrtStatus* ReadEpContextDataFromFile(const OrtApi& api, const char* file_name, const OrtGraph* graph, + std::vector& data) { + std::filesystem::path data_path; + RETURN_IF_ERROR(ResolveEpContextDataPath(api, file_name, graph, data_path)); + std::ifstream input_stream(data_path, std::ios::binary); + if (!input_stream) { + const std::string message = "Failed to open EPContext data file for read: " + + PathToUtf8String(data_path); + return api.CreateStatus(ORT_FAIL, message.c_str()); + } + + data.assign(std::istreambuf_iterator{input_stream}, std::istreambuf_iterator{}); + if (input_stream.bad()) { + const std::string message = "Failed to read EPContext data file: " + + PathToUtf8String(data_path); + return api.CreateStatus(ORT_FAIL, message.c_str()); + } + + return nullptr; +} + +OrtStatus* WriteEpContextDataToFile(const OrtApi& api, const char* file_name, const OrtGraph* graph, + const void* buffer, size_t buffer_size) { + std::filesystem::path data_path; + RETURN_IF_ERROR(ResolveEpContextDataPath(api, file_name, graph, data_path)); + std::ofstream output_stream(data_path, std::ios::binary); + if (!output_stream) { + const std::string message = "Failed to open EPContext data file for write: " + + PathToUtf8String(data_path); + return api.CreateStatus(ORT_FAIL, message.c_str()); + } + + if (buffer_size != 0) { + if (buffer_size > static_cast(std::numeric_limits::max())) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data buffer is too large to write"); + } + + output_stream.write(static_cast(buffer), static_cast(buffer_size)); + if (!output_stream) { + const std::string message = "Failed to write EPContext data file: " + + PathToUtf8String(data_path); + return api.CreateStatus(ORT_FAIL, message.c_str()); + } + } + + return nullptr; +} + +} // namespace + const FloatInitializer* MulKernel::TryGetSavedInitializer(const std::string& name) const { auto iter = float_initializers.find(name); return iter != float_initializers.end() ? &iter->second : nullptr; @@ -168,14 +298,14 @@ struct EpContextNodeComputeInfo : NodeComputeInfoBase { }; ExampleEp::ExampleEp(ExampleEpFactory& factory, const std::string& name, const Config& config, const OrtLogger& logger, - OrtEpContextConfig* ep_context_config) + Ort::EpContextConfig ep_context_config) : OrtEp{}, // explicitly call the struct ctor to ensure all optional values are default initialized ApiPtrs{static_cast(factory)}, factory_{factory}, name_{name}, config_{config}, logger_{logger}, - ep_context_config_{ep_context_config} { + ep_context_config_{std::move(ep_context_config)} { ort_version_supported = ORT_API_VERSION; // set to the ORT version we were compiled with. // Initialize the execution provider's function table @@ -195,9 +325,7 @@ ExampleEp::ExampleEp(ExampleEpFactory& factory, const std::string& name, const C ORT_FILE, __LINE__, __FUNCTION__)); } -ExampleEp::~ExampleEp() { - ep_api.ReleaseEpContextConfig(ep_context_config_); -} +ExampleEp::~ExampleEp() = default; /*static*/ const char* ORT_API_CALL ExampleEp ::GetNameImpl(const OrtEp* this_ptr) noexcept { @@ -424,14 +552,35 @@ OrtStatus* ORT_API_CALL ExampleEp::CompileImpl(_In_ OrtEp* this_ptr, _In_ const std::string ep_cache_context; RETURN_IF_ERROR(ep_cache_context_attr.GetValue(ep_cache_context)); - Ort::AllocatorWithDefaultOptions allocator; - void* ep_context_data = nullptr; - size_t ep_context_data_size = 0; - RETURN_IF_ERROR(ep->ep_api.ReadEpContextData(ep->ep_context_config_, ep_cache_context.c_str(), ort_graphs[0], - allocator, &ep_context_data, &ep_context_data_size)); - (void)ep_context_data_size; - if (ep_context_data != nullptr) { - allocator.Free(ep_context_data); + OrtReadEpContextDataFunc read_func = nullptr; + void* read_state = nullptr; + RETURN_IF_ERROR(ep->ep_api.EpContextConfig_GetEpContextDataReadFunc(ep->ep_context_config_, &read_func, + &read_state)); + if (read_func != nullptr) { + Ort::AllocatorWithDefaultOptions allocator; + void* ep_context_data = nullptr; + size_t ep_context_data_size = 0; + OrtStatus* status = read_func(read_state, ep_cache_context.c_str(), allocator, &ep_context_data, + &ep_context_data_size); + if (status != nullptr) { + if (ep_context_data != nullptr) { + allocator.Free(ep_context_data); + } + return status; + } + + if (ep_context_data_size != 0 && ep_context_data == nullptr) { + return ep->ort_api.CreateStatus(ORT_FAIL, + "OrtReadEpContextDataFunc returned a null buffer for non-empty EPContext data"); + } + + if (ep_context_data != nullptr) { + allocator.Free(ep_context_data); + } + } else { + std::vector ep_context_data; + RETURN_IF_ERROR(ReadEpContextDataFromFile(ep->ort_api, ep_cache_context.c_str(), ort_graphs[0], + ep_context_data)); } } @@ -546,8 +695,16 @@ OrtStatus* ExampleEp::CreateEpContextNodes(const OrtGraph* graph, std::string ep_ctx = config_.embed_ep_context_in_model ? "binary_data" : fused_node_name + ".ctx"; if (!config_.embed_ep_context_in_model) { const std::string ep_context_data = "binary_data"; - RETURN_IF_ERROR(ep_api.WriteEpContextData(ep_context_config_, ep_ctx.c_str(), graph, - ep_context_data.data(), ep_context_data.size())); + OrtWriteEpContextDataFunc write_func = nullptr; + void* write_state = nullptr; + RETURN_IF_ERROR(ep_api.EpContextConfig_GetEpContextDataWriteFunc(ep_context_config_, &write_func, + &write_state)); + if (write_func != nullptr) { + RETURN_IF_ERROR(write_func(write_state, ep_ctx.c_str(), ep_context_data.data(), ep_context_data.size())); + } else { + RETURN_IF_ERROR(WriteEpContextDataToFile(ort_api, ep_ctx.c_str(), graph, + ep_context_data.data(), ep_context_data.size())); + } } attributes[0] = Ort::OpAttr("ep_cache_context", ep_ctx.data(), static_cast(ep_ctx.size()), ORT_OP_ATTR_STRING); diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep.h b/onnxruntime/test/autoep/library/example_plugin_ep/ep.h index 8f6080fe3d46a..b20ae8fe10825 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep.h +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep.h @@ -67,7 +67,7 @@ class ExampleEp : public OrtEp, public ApiPtrs { }; ExampleEp(ExampleEpFactory& factory, const std::string& name, const Config& config, const OrtLogger& logger, - OrtEpContextConfig* ep_context_config); + Ort::EpContextConfig ep_context_config); ~ExampleEp(); @@ -125,7 +125,7 @@ class ExampleEp : public OrtEp, public ApiPtrs { std::string name_; Config config_{}; const OrtLogger& logger_; - OrtEpContextConfig* ep_context_config_ = nullptr; + Ort::EpContextConfig ep_context_config_{nullptr}; std::unordered_map> mul_kernels_; std::unordered_map> ep_context_kernels_; std::unordered_map float_initializers_; diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc index 668be73c31f3b..a72a9c2b4221c 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc @@ -233,17 +233,11 @@ OrtStatus* ORT_API_CALL ExampleEpFactory::CreateEpImpl(OrtEpFactory* this_ptr, config.embed_ep_context_in_model = ep_context_embed_mode != "0"; config.enable_weightless_ep_context_nodes = weightless_ep_context_nodes_enable == "1"; - auto release_ep_context_config = [factory](OrtEpContextConfig* config_to_release) { - factory->ep_api.ReleaseEpContextConfig(config_to_release); - }; - std::unique_ptr ep_context_config{ - nullptr, release_ep_context_config}; OrtEpContextConfig* ep_context_config_raw = nullptr; RETURN_IF_ERROR(factory->ep_api.SessionOptions_GetEpContextConfig(session_options, &ep_context_config_raw)); - ep_context_config.reset(ep_context_config_raw); - + Ort::EpContextConfig ep_context_config{ep_context_config_raw}; auto dummy_ep = std::make_unique(*factory, factory->ep_name_, config, *logger, - ep_context_config.release()); + std::move(ep_context_config)); *ep = dummy_ep.release(); return nullptr; diff --git a/onnxruntime/test/framework/ep_plugin_provider_test.cc b/onnxruntime/test/framework/ep_plugin_provider_test.cc index a6a0b01261433..e186aed6bf427 100644 --- a/onnxruntime/test/framework/ep_plugin_provider_test.cc +++ b/onnxruntime/test/framework/ep_plugin_provider_test.cc @@ -69,18 +69,6 @@ static void ExpectOrtStatus(OrtStatus* status_ptr, OrtErrorCode expected_code, c EXPECT_THAT(status.GetErrorMessage(), ::testing::HasSubstr(expected_message)); } -static void ExpectOrtStatusNotOk(OrtStatus* status_ptr) { - Ort::Status status{status_ptr}; - EXPECT_FALSE(status.IsOK()); -} - -static std::filesystem::path MakeEpContextDataTestDir(const char* test_name) { - std::filesystem::path test_dir = std::filesystem::temp_directory_path() / test_name; - std::filesystem::remove_all(test_dir); - std::filesystem::create_directories(test_dir); - return test_dir; -} - struct EpContextReadCallbackState { bool called = false; std::string file_name; @@ -109,26 +97,6 @@ static OrtStatus* ORT_API_CALL EpContextReadCallback(void* state, const char* fi return nullptr; } -struct EpContextCallbackErrorState { - OrtErrorCode error_code = ORT_FAIL; - const char* message = nullptr; -}; - -static OrtStatus* ORT_API_CALL EpContextFailingReadCallback(void* state, const char* /*file_name*/, - OrtAllocator* /*allocator*/, void** /*buffer*/, - size_t* /*data_size*/) { - const auto* error_state = static_cast(state); - return Ort::GetApi().CreateStatus(error_state->error_code, error_state->message); -} - -static OrtStatus* ORT_API_CALL EpContextNonEmptyNullBufferReadCallback(void* /*state*/, const char* /*file_name*/, - OrtAllocator* /*allocator*/, void** buffer, - size_t* data_size) { - *buffer = nullptr; - *data_size = 4; - return nullptr; -} - struct EpContextWriteCallbackState { bool called = false; std::string file_name; @@ -147,12 +115,6 @@ static OrtStatus* ORT_API_CALL EpContextWriteCallback(void* state, const char* f return nullptr; } -static OrtStatus* ORT_API_CALL EpContextFailingWriteCallback(void* state, const char* /*file_name*/, - const void* /*buffer*/, size_t /*buffer_size*/) { - const auto* error_state = static_cast(state); - return Ort::GetApi().CreateStatus(error_state->error_code, error_state->message); -} - // Normally, a plugin EP would be implemented in a separate library. // The `test_plugin_ep` namespace contains a local implementation intended for unit testing. namespace test_plugin_ep { @@ -1813,33 +1775,43 @@ TEST(PluginExecutionProviderTest, GetGraphCaptureNodeAssignmentPolicy) { } } -TEST(PluginExecutionProviderTest, EpContextDataReadFuncIsCalledViaEpApi) { +TEST(PluginExecutionProviderTest, EpContextDataReadFuncIsReturnedByEpApi) { const auto& ep_api = Ort::GetEpApi(); Ort::SessionOptions session_options; - EpContextReadCallbackState read_state{ + EpContextReadCallbackState callback_state{ false, {}, {'e', 'p', 'c', 't', 'x'}, }; - session_options.SetEpContextDataReadFunc(EpContextReadCallback, &read_state); + session_options.SetEpContextDataReadFunc(EpContextReadCallback, &callback_state); OrtEpContextConfig* ep_context_config = nullptr; ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(session_options, &ep_context_config)); auto release_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(ep_context_config); }); + OrtReadEpContextDataFunc read_func = nullptr; + void* callback_state_out = nullptr; + ASSERT_ORTSTATUS_OK(ep_api.EpContextConfig_GetEpContextDataReadFunc(ep_context_config, &read_func, + &callback_state_out)); + ASSERT_EQ(read_func, EpContextReadCallback); + ASSERT_EQ(callback_state_out, &callback_state); + Ort::AllocatorWithDefaultOptions allocator; void* buffer = nullptr; size_t buffer_size = 0; - ASSERT_ORTSTATUS_OK(ep_api.ReadEpContextData(ep_context_config, "context.bin", nullptr, allocator, - &buffer, &buffer_size)); - auto release_buffer = gsl::finally([&]() { allocator.Free(buffer); }); + ASSERT_ORTSTATUS_OK(read_func(callback_state_out, "context.bin", allocator, &buffer, &buffer_size)); + auto release_buffer = gsl::finally([&]() { + if (buffer != nullptr) { + allocator.Free(buffer); + } + }); - ASSERT_TRUE(read_state.called); - EXPECT_EQ(read_state.file_name, "context.bin"); - ASSERT_EQ(buffer_size, read_state.payload.size()); + ASSERT_TRUE(callback_state.called); + EXPECT_EQ(callback_state.file_name, "context.bin"); + ASSERT_EQ(buffer_size, callback_state.payload.size()); EXPECT_EQ(std::vector(static_cast(buffer), static_cast(buffer) + buffer_size), - read_state.payload); + callback_state.payload); } TEST(PluginExecutionProviderTest, EpContextDataApiRejectsInvalidArguments) { @@ -1858,23 +1830,24 @@ TEST(PluginExecutionProviderTest, EpContextDataApiRejectsInvalidArguments) { ExpectOrtStatus(ort_api.SessionOptions_SetEpContextDataReadFunc(session_options, nullptr, nullptr), ORT_INVALID_ARGUMENT, "'read_func' parameter must not be NULL"); - Ort::AllocatorWithDefaultOptions allocator; - void* buffer = nullptr; - size_t buffer_size = 0; - ExpectOrtStatus(ep_api.ReadEpContextData(nullptr, nullptr, nullptr, allocator, &buffer, &buffer_size), - ORT_INVALID_ARGUMENT, "file_name is NULL"); - ExpectOrtStatus(ep_api.ReadEpContextData(nullptr, "context.bin", nullptr, nullptr, &buffer, &buffer_size), - ORT_INVALID_ARGUMENT, "OrtAllocator is NULL"); - ExpectOrtStatus(ep_api.ReadEpContextData(nullptr, "context.bin", nullptr, allocator, nullptr, &buffer_size), - ORT_INVALID_ARGUMENT, "Output buffer is NULL"); - ExpectOrtStatus(ep_api.ReadEpContextData(nullptr, "context.bin", nullptr, allocator, &buffer, nullptr), - ORT_INVALID_ARGUMENT, "Output buffer_size is NULL"); - - const std::vector payload{'x'}; - ExpectOrtStatus(ep_api.WriteEpContextData(nullptr, nullptr, nullptr, payload.data(), payload.size()), - ORT_INVALID_ARGUMENT, "file_name is NULL"); - ExpectOrtStatus(ep_api.WriteEpContextData(nullptr, "context.bin", nullptr, nullptr, payload.size()), - ORT_INVALID_ARGUMENT, "EPContext data buffer is NULL for non-empty data"); + ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(session_options, &ep_context_config)); + auto release_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(ep_context_config); }); + + OrtReadEpContextDataFunc read_func = nullptr; + OrtWriteEpContextDataFunc write_func = nullptr; + void* state = nullptr; + ExpectOrtStatus(ep_api.EpContextConfig_GetEpContextDataReadFunc(nullptr, &read_func, &state), + ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); + ExpectOrtStatus(ep_api.EpContextConfig_GetEpContextDataReadFunc(ep_context_config, nullptr, &state), + ORT_INVALID_ARGUMENT, "Output read_func is NULL"); + ExpectOrtStatus(ep_api.EpContextConfig_GetEpContextDataReadFunc(ep_context_config, &read_func, nullptr), + ORT_INVALID_ARGUMENT, "Output state is NULL"); + ExpectOrtStatus(ep_api.EpContextConfig_GetEpContextDataWriteFunc(nullptr, &write_func, &state), + ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); + ExpectOrtStatus(ep_api.EpContextConfig_GetEpContextDataWriteFunc(ep_context_config, nullptr, &state), + ORT_INVALID_ARGUMENT, "Output write_func is NULL"); + ExpectOrtStatus(ep_api.EpContextConfig_GetEpContextDataWriteFunc(ep_context_config, &write_func, nullptr), + ORT_INVALID_ARGUMENT, "Output state is NULL"); #if !defined(ORT_MINIMAL_BUILD) Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "EpContextDataApiRejectsInvalidArguments"}; @@ -1889,211 +1862,88 @@ TEST(PluginExecutionProviderTest, EpContextDataApiRejectsInvalidArguments) { #endif // !defined(ORT_MINIMAL_BUILD) } -TEST(PluginExecutionProviderTest, EpContextDataCallbackErrorsArePropagated) { +TEST(PluginExecutionProviderTest, EpContextDataAccessorsReturnNullWhenCallbacksUnset) { const auto& ep_api = Ort::GetEpApi(); Ort::SessionOptions session_options; - EpContextCallbackErrorState read_error{ORT_FAIL, "read callback failed"}; - session_options.SetEpContextDataReadFunc(EpContextFailingReadCallback, &read_error); - OrtEpContextConfig* ep_context_config = nullptr; ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(session_options, &ep_context_config)); auto release_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(ep_context_config); }); - Ort::AllocatorWithDefaultOptions allocator; - void* buffer = nullptr; - size_t buffer_size = 0; - ExpectOrtStatus(ep_api.ReadEpContextData(ep_context_config, "context.bin", nullptr, allocator, - &buffer, &buffer_size), - ORT_FAIL, "read callback failed"); + OrtReadEpContextDataFunc read_func = EpContextReadCallback; + OrtWriteEpContextDataFunc write_func = EpContextWriteCallback; + void* state = reinterpret_cast(0x1); -#if !defined(ORT_MINIMAL_BUILD) - Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "EpContextDataCallbackErrorsArePropagated"}; - Ort::ModelCompilationOptions compilation_options{env, session_options}; - EpContextCallbackErrorState write_error{ORT_EP_FAIL, "write callback failed"}; - compilation_options.SetEpContextDataWriteFunc(EpContextFailingWriteCallback, &write_error); + ASSERT_ORTSTATUS_OK(ep_api.EpContextConfig_GetEpContextDataReadFunc(ep_context_config, &read_func, &state)); + EXPECT_EQ(read_func, nullptr); + EXPECT_EQ(state, nullptr); - const auto* internal_options = reinterpret_cast( - static_cast(compilation_options)); - OrtEpContextConfig* write_config = nullptr; - ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(&internal_options->GetSessionOptions(), &write_config)); - auto release_write_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(write_config); }); - - const std::vector payload{'x'}; - ExpectOrtStatus(ep_api.WriteEpContextData(write_config, "context.bin", nullptr, payload.data(), payload.size()), - ORT_EP_FAIL, "write callback failed"); -#endif // !defined(ORT_MINIMAL_BUILD) + ASSERT_ORTSTATUS_OK(ep_api.EpContextConfig_GetEpContextDataWriteFunc(ep_context_config, &write_func, &state)); + EXPECT_EQ(write_func, nullptr); + EXPECT_EQ(state, nullptr); } -TEST(PluginExecutionProviderTest, EpContextDataAllowsEmptyPayloads) { - const auto& ep_api = Ort::GetEpApi(); +TEST(PluginExecutionProviderTest, EpContextConfigCxxWrapperReturnsCallbacks) { Ort::SessionOptions session_options; - EpContextReadCallbackState read_state{}; - session_options.SetEpContextDataReadFunc(EpContextReadCallback, &read_state); + EpContextReadCallbackState callback_state{}; + session_options.SetEpContextDataReadFunc(EpContextReadCallback, &callback_state); - OrtEpContextConfig* ep_context_config = nullptr; - ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(session_options, &ep_context_config)); - auto release_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(ep_context_config); }); + Ort::EpContextConfig ep_context_config{session_options}; + auto [read_func, read_state] = ep_context_config.GetEpContextDataReadFunc(); + EXPECT_EQ(read_func, EpContextReadCallback); + EXPECT_EQ(read_state, &callback_state); - Ort::AllocatorWithDefaultOptions allocator; - void* buffer = reinterpret_cast(0x1); - size_t buffer_size = 1; - ASSERT_ORTSTATUS_OK(ep_api.ReadEpContextData(ep_context_config, "empty.bin", nullptr, allocator, - &buffer, &buffer_size)); - EXPECT_TRUE(read_state.called); - EXPECT_EQ(read_state.file_name, "empty.bin"); - EXPECT_EQ(buffer, nullptr); - EXPECT_EQ(buffer_size, 0U); - -#if !defined(ORT_MINIMAL_BUILD) - Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "EpContextDataAllowsEmptyPayloads"}; - Ort::ModelCompilationOptions compilation_options{env, session_options}; - EpContextWriteCallbackState write_state{}; - compilation_options.SetEpContextDataWriteFunc(EpContextWriteCallback, &write_state); - - const auto* internal_options = reinterpret_cast( - static_cast(compilation_options)); - OrtEpContextConfig* write_config = nullptr; - ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(&internal_options->GetSessionOptions(), &write_config)); - auto release_write_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(write_config); }); - - ASSERT_ORTSTATUS_OK(ep_api.WriteEpContextData(write_config, "empty.bin", nullptr, nullptr, 0)); - EXPECT_TRUE(write_state.called); - EXPECT_EQ(write_state.file_name, "empty.bin"); - EXPECT_TRUE(write_state.payload.empty()); -#endif // !defined(ORT_MINIMAL_BUILD) -} - -TEST(PluginExecutionProviderTest, EpContextDataReadRejectsNonEmptyNullCallbackBuffer) { - const auto& ep_api = Ort::GetEpApi(); - Ort::SessionOptions session_options; - session_options.SetEpContextDataReadFunc(EpContextNonEmptyNullBufferReadCallback, nullptr); - - OrtEpContextConfig* ep_context_config = nullptr; - ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(session_options, &ep_context_config)); - auto release_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(ep_context_config); }); - - Ort::AllocatorWithDefaultOptions allocator; - void* buffer = nullptr; - size_t buffer_size = 0; - ExpectOrtStatus(ep_api.ReadEpContextData(ep_context_config, "context.bin", nullptr, allocator, - &buffer, &buffer_size), - ORT_FAIL, "returned a null buffer for non-empty EPContext data"); + auto [write_func, write_state] = ep_context_config.GetEpContextDataWriteFunc(); + EXPECT_EQ(write_func, nullptr); + EXPECT_EQ(write_state, nullptr); } #if !defined(ORT_MINIMAL_BUILD) -TEST(PluginExecutionProviderTest, EpContextDataWriteFuncIsCalledViaEpApi) { - const auto& ep_api = Ort::GetEpApi(); - Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "EpContextDataWriteFuncIsCalledViaEpApi"}; +TEST(PluginExecutionProviderTest, EpContextDataWriteFuncIsReturnedByEpApi) { + Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "EpContextDataWriteFuncIsReturnedByEpApi"}; Ort::SessionOptions session_options; Ort::ModelCompilationOptions compilation_options{env, session_options}; - EpContextWriteCallbackState write_state{}; - compilation_options.SetEpContextDataWriteFunc(EpContextWriteCallback, &write_state); + EpContextWriteCallbackState callback_state{}; + compilation_options.SetEpContextDataWriteFunc(EpContextWriteCallback, &callback_state); const auto* internal_options = reinterpret_cast( static_cast(compilation_options)); - OrtEpContextConfig* ep_context_config = nullptr; - ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(&internal_options->GetSessionOptions(), - &ep_context_config)); - auto release_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(ep_context_config); }); + Ort::EpContextConfig ep_context_config{&internal_options->GetSessionOptions()}; + auto [write_func, callback_state_out] = ep_context_config.GetEpContextDataWriteFunc(); + ASSERT_EQ(write_func, EpContextWriteCallback); + ASSERT_EQ(callback_state_out, &callback_state); const std::vector payload{'b', 'i', 'n', 'a', 'r', 'y'}; - ASSERT_ORTSTATUS_OK(ep_api.WriteEpContextData(ep_context_config, "engine.bin", nullptr, - payload.data(), payload.size())); + ASSERT_ORTSTATUS_OK(write_func(callback_state_out, "engine.bin", payload.data(), payload.size())); - ASSERT_TRUE(write_state.called); - EXPECT_EQ(write_state.file_name, "engine.bin"); - EXPECT_EQ(write_state.payload, payload); + ASSERT_TRUE(callback_state.called); + EXPECT_EQ(callback_state.file_name, "engine.bin"); + EXPECT_EQ(callback_state.payload, payload); } #endif // !defined(ORT_MINIMAL_BUILD) -TEST(PluginExecutionProviderTest, EpContextDataFallsBackToDisk) { - const auto& ep_api = Ort::GetEpApi(); - const std::filesystem::path test_dir = std::filesystem::temp_directory_path() / "ort_ep_context_data_test"; - std::filesystem::create_directories(test_dir); - const std::filesystem::path data_path = test_dir / "context.bin"; - const std::string data_path_utf8 = PathToUTF8String(data_path.native()); - auto cleanup = gsl::finally([&]() { - std::error_code ec; - std::filesystem::remove(data_path, ec); - std::filesystem::remove(test_dir, ec); - }); - - const std::vector payload{'d', 'i', 's', 'k'}; - ASSERT_ORTSTATUS_OK(ep_api.WriteEpContextData(nullptr, data_path_utf8.c_str(), nullptr, - payload.data(), payload.size())); - - Ort::AllocatorWithDefaultOptions allocator; - void* buffer = nullptr; - size_t buffer_size = 0; - ASSERT_ORTSTATUS_OK(ep_api.ReadEpContextData(nullptr, data_path_utf8.c_str(), nullptr, allocator, - &buffer, &buffer_size)); - auto release_buffer = gsl::finally([&]() { allocator.Free(buffer); }); - - ASSERT_EQ(buffer_size, payload.size()); - EXPECT_EQ(std::vector(static_cast(buffer), static_cast(buffer) + buffer_size), payload); -} - -TEST(PluginExecutionProviderTest, EpContextDataDiskFallbackResolvesRelativePathAgainstGraphModelPath) { - const auto& ep_api = Ort::GetEpApi(); - const std::filesystem::path test_dir = MakeEpContextDataTestDir("ort_ep_context_data_relative_path_test"); - auto cleanup = gsl::finally([&]() { - std::error_code ec; - std::filesystem::remove_all(test_dir, ec); - }); - - const std::filesystem::path source_model_path{ORT_TSTR("testdata/add_mul_add.onnx")}; - const std::filesystem::path model_path = test_dir / "model.onnx"; - std::filesystem::copy_file(source_model_path, model_path, std::filesystem::copy_options::overwrite_existing); - - std::shared_ptr model; - ASSERT_STATUS_OK(Model::Load(model_path.native().c_str(), model, nullptr, - DefaultLoggingManager().DefaultLogger())); - GraphViewer graph_viewer(model->MainGraph()); - std::unique_ptr ep_graph = nullptr; - ASSERT_STATUS_OK(EpGraph::Create(graph_viewer, ep_graph, true)); +TEST(PluginExecutionProviderTest, EpContextDataReturnedReadFuncAllowsEmptyPayloads) { + Ort::SessionOptions session_options; - const std::vector payload{'r', 'e', 'l'}; - ASSERT_ORTSTATUS_OK(ep_api.WriteEpContextData(nullptr, "context.bin", ep_graph.get(), - payload.data(), payload.size())); + EpContextReadCallbackState callback_state{}; + session_options.SetEpContextDataReadFunc(EpContextReadCallback, &callback_state); - const std::filesystem::path expected_context_path = test_dir / "context.bin"; - ASSERT_TRUE(std::filesystem::exists(expected_context_path)); + Ort::EpContextConfig ep_context_config{session_options}; + auto [read_func, read_state] = ep_context_config.GetEpContextDataReadFunc(); + ASSERT_EQ(read_func, EpContextReadCallback); + ASSERT_EQ(read_state, &callback_state); Ort::AllocatorWithDefaultOptions allocator; - void* buffer = nullptr; - size_t buffer_size = 0; - ASSERT_ORTSTATUS_OK(ep_api.ReadEpContextData(nullptr, "context.bin", ep_graph.get(), allocator, - &buffer, &buffer_size)); - auto release_buffer = gsl::finally([&]() { allocator.Free(buffer); }); - - ASSERT_EQ(buffer_size, payload.size()); - EXPECT_EQ(std::vector(static_cast(buffer), static_cast(buffer) + buffer_size), payload); -} - -TEST(PluginExecutionProviderTest, EpContextDataDiskFallbackReportsFileErrors) { - const auto& ep_api = Ort::GetEpApi(); - const std::filesystem::path test_dir = MakeEpContextDataTestDir("ort_ep_context_data_file_error_test"); - auto cleanup = gsl::finally([&]() { - std::error_code ec; - std::filesystem::remove_all(test_dir, ec); - }); - - const std::filesystem::path missing_file_path = test_dir / "missing" / "context.bin"; - const std::string missing_file_path_utf8 = PathToUTF8String(missing_file_path.native()); - const std::vector payload{'x'}; - - ExpectOrtStatus(ep_api.WriteEpContextData(nullptr, missing_file_path_utf8.c_str(), nullptr, - payload.data(), payload.size()), - ORT_FAIL, "Failed to open EPContext data file for write"); + void* buffer = reinterpret_cast(0x1); + size_t buffer_size = 1; + ASSERT_ORTSTATUS_OK(read_func(read_state, "empty.bin", allocator, &buffer, &buffer_size)); - Ort::AllocatorWithDefaultOptions allocator; - void* buffer = nullptr; - size_t buffer_size = 0; - ExpectOrtStatusNotOk(ep_api.ReadEpContextData(nullptr, missing_file_path_utf8.c_str(), nullptr, allocator, - &buffer, &buffer_size)); + EXPECT_TRUE(callback_state.called); + EXPECT_EQ(callback_state.file_name, "empty.bin"); + EXPECT_EQ(buffer, nullptr); + EXPECT_EQ(buffer_size, 0U); } // Helper: create a no-threshold resource accountant via the real factory (config ","). From 34913c0ddffd166f63986364e43bf72b6e7d8763 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Thu, 21 May 2026 19:43:29 -0700 Subject: [PATCH 07/48] Add sample EPContext data fallback helpers --- cmake/onnxruntime_unittests.cmake | 1 + .../autoep/library/ep_context_data_utils.h | 229 ++++++++++++++++++ .../autoep/library/example_plugin_ep/ep.cc | 179 +------------- 3 files changed, 238 insertions(+), 171 deletions(-) create mode 100644 onnxruntime/test/autoep/library/ep_context_data_utils.h diff --git a/cmake/onnxruntime_unittests.cmake b/cmake/onnxruntime_unittests.cmake index 23eccb22476df..d95bf0c4c6189 100644 --- a/cmake/onnxruntime_unittests.cmake +++ b/cmake/onnxruntime_unittests.cmake @@ -2174,6 +2174,7 @@ if (onnxruntime_BUILD_SHARED_LIB AND # file(GLOB onnxruntime_autoep_test_library_src "${TEST_SRC_DIR}/autoep/library/example_plugin_ep/*.h" "${TEST_SRC_DIR}/autoep/library/example_plugin_ep/*.cc" + "${TEST_SRC_DIR}/autoep/library/ep_context_data_utils.h" "${TEST_SRC_DIR}/autoep/library/plugin_ep_utils.h") onnxruntime_add_shared_library_module(example_plugin_ep ${onnxruntime_autoep_test_library_src}) target_include_directories(example_plugin_ep PRIVATE ${REPO_ROOT}/include/onnxruntime/core/session) diff --git a/onnxruntime/test/autoep/library/ep_context_data_utils.h b/onnxruntime/test/autoep/library/ep_context_data_utils.h new file mode 100644 index 0000000000000..b866973c0b48f --- /dev/null +++ b/onnxruntime/test/autoep/library/ep_context_data_utils.h @@ -0,0 +1,229 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +#include "plugin_ep_utils.h" + +// Sample-only EPContext data helpers. These are intentionally outside the ORT C and EP ABI. +namespace ep_context_data_utils { + +#ifdef _WIN32 +inline std::wstring Utf8ToWideString(std::string_view value) { + if (value.empty() || value.size() > static_cast(std::numeric_limits::max())) { + return {}; + } + + const int wide_length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), + static_cast(value.size()), nullptr, 0); + if (wide_length <= 0) { + return {}; + } + + std::wstring wide_value(static_cast(wide_length), L'\0'); + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), static_cast(value.size()), + wide_value.data(), wide_length); + return wide_value; +} + +inline std::string WideToUtf8String(std::wstring_view value) { + if (value.empty() || value.size() > static_cast(std::numeric_limits::max())) { + return {}; + } + + const int utf8_length = WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast(value.size()), + nullptr, 0, nullptr, nullptr); + if (utf8_length <= 0) { + return {}; + } + + std::string utf8_value(static_cast(utf8_length), '\0'); + WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast(value.size()), utf8_value.data(), utf8_length, + nullptr, nullptr); + return utf8_value; +} +#endif + +inline std::filesystem::path Utf8Path(const char* path) { +#ifdef _WIN32 + return std::filesystem::path{Utf8ToWideString(path)}; +#else + return std::filesystem::path{path}; +#endif +} + +inline std::string PathToUtf8String(const std::filesystem::path& path) { +#ifdef _WIN32 + return WideToUtf8String(path.wstring()); +#else + return path.string(); +#endif +} + +inline OrtStatus* ResolveEpContextDataPath(const OrtApi& api, const char* file_name, const OrtGraph* graph, + std::filesystem::path& data_path) { + if (file_name == nullptr || file_name[0] == '\0') { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); + } + + data_path = Utf8Path(file_name); + if (data_path.empty()) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name is not a valid path"); + } + + if (data_path.is_absolute() || graph == nullptr) { + return nullptr; + } + + const ORTCHAR_T* model_path = nullptr; + RETURN_IF_ERROR(api.Graph_GetModelPath(graph, &model_path)); + if (model_path == nullptr || model_path[0] == 0) { + return nullptr; + } + + data_path = std::filesystem::path{model_path}.parent_path() / data_path; + return nullptr; +} + +inline OrtStatus* ReadEpContextDataFromFile(const OrtApi& api, const char* file_name, const OrtGraph* graph, + std::vector& data) { + data.clear(); + + std::filesystem::path data_path; + RETURN_IF_ERROR(ResolveEpContextDataPath(api, file_name, graph, data_path)); + + std::ifstream input_stream(data_path, std::ios::binary); + if (!input_stream) { + const std::string message = "Failed to open EPContext data file for read: " + + PathToUtf8String(data_path); + return api.CreateStatus(ORT_FAIL, message.c_str()); + } + + data.assign(std::istreambuf_iterator{input_stream}, std::istreambuf_iterator{}); + if (input_stream.bad()) { + const std::string message = "Failed to read EPContext data file: " + + PathToUtf8String(data_path); + return api.CreateStatus(ORT_FAIL, message.c_str()); + } + + return nullptr; +} + +inline OrtStatus* WriteEpContextDataToFile(const OrtApi& api, const char* file_name, const OrtGraph* graph, + const void* buffer, size_t buffer_size) { + if (buffer == nullptr && buffer_size != 0) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data buffer must not be null for non-empty data"); + } + + std::filesystem::path data_path; + RETURN_IF_ERROR(ResolveEpContextDataPath(api, file_name, graph, data_path)); + + std::ofstream output_stream(data_path, std::ios::binary); + if (!output_stream) { + const std::string message = "Failed to open EPContext data file for write: " + + PathToUtf8String(data_path); + return api.CreateStatus(ORT_FAIL, message.c_str()); + } + + if (buffer_size != 0) { + if (buffer_size > static_cast(std::numeric_limits::max())) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data buffer is too large to write"); + } + + output_stream.write(static_cast(buffer), static_cast(buffer_size)); + if (!output_stream) { + const std::string message = "Failed to write EPContext data file: " + + PathToUtf8String(data_path); + return api.CreateStatus(ORT_FAIL, message.c_str()); + } + } + + return nullptr; +} + +inline OrtStatus* ReadEpContextDataWithFileFallback(const OrtApi& api, const OrtEpApi& ep_api, + const OrtEpContextConfig* ep_context_config, + const char* file_name, const OrtGraph* graph, + std::vector& data) { + if (file_name == nullptr || file_name[0] == '\0') { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); + } + + OrtReadEpContextDataFunc read_func = nullptr; + void* read_state = nullptr; + if (ep_context_config != nullptr) { + RETURN_IF_ERROR(ep_api.EpContextConfig_GetEpContextDataReadFunc(ep_context_config, &read_func, &read_state)); + } + + if (read_func == nullptr) { + return ReadEpContextDataFromFile(api, file_name, graph, data); + } + + Ort::AllocatorWithDefaultOptions allocator; + void* ep_context_data = nullptr; + size_t ep_context_data_size = 0; + OrtStatus* status = read_func(read_state, file_name, allocator, &ep_context_data, &ep_context_data_size); + auto buffer_deleter = [&allocator](void* buffer_to_free) { + if (buffer_to_free != nullptr) { + allocator.Free(buffer_to_free); + } + }; + std::unique_ptr ep_context_data_guard(ep_context_data, buffer_deleter); + + if (status != nullptr) { + return status; + } + + if (ep_context_data_size != 0 && ep_context_data == nullptr) { + return api.CreateStatus( + ORT_FAIL, "OrtReadEpContextDataFunc returned a null buffer for non-empty EPContext data"); + } + + data.clear(); + if (ep_context_data != nullptr) { + const char* ep_context_data_begin = static_cast(ep_context_data); + data.assign(ep_context_data_begin, ep_context_data_begin + ep_context_data_size); + } + + return nullptr; +} + +inline OrtStatus* WriteEpContextDataWithFileFallback(const OrtApi& api, const OrtEpApi& ep_api, + const OrtEpContextConfig* ep_context_config, + const char* file_name, const OrtGraph* graph, + const void* buffer, size_t buffer_size) { + if (file_name == nullptr || file_name[0] == '\0') { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); + } + + if (buffer == nullptr && buffer_size != 0) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data buffer must not be null for non-empty data"); + } + + OrtWriteEpContextDataFunc write_func = nullptr; + void* write_state = nullptr; + if (ep_context_config != nullptr) { + RETURN_IF_ERROR(ep_api.EpContextConfig_GetEpContextDataWriteFunc(ep_context_config, &write_func, &write_state)); + } + + if (write_func != nullptr) { + return write_func(write_state, file_name, buffer, buffer_size); + } + + return WriteEpContextDataToFile(api, file_name, graph, buffer, buffer_size); +} + +} // namespace ep_context_data_utils diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc index 3f4a25a8a7248..6818e85e698c7 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc @@ -7,147 +7,17 @@ #include #include #include -#include -#include -#include -#include -#include #include #include -#include #include #include -#ifdef _WIN32 -#include -#endif - +#include "../ep_context_data_utils.h" #include "ep_factory.h" #include "ep_stream_support.h" extern std::atomic g_sync_count; -namespace { - -#ifdef _WIN32 -std::wstring Utf8ToWideString(std::string_view value) { - if (value.empty() || value.size() > static_cast(std::numeric_limits::max())) { - return {}; - } - - const int wide_length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), - static_cast(value.size()), nullptr, 0); - if (wide_length <= 0) { - return {}; - } - - std::wstring wide_value(static_cast(wide_length), L'\0'); - MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), static_cast(value.size()), - wide_value.data(), wide_length); - return wide_value; -} - -std::string WideToUtf8String(std::wstring_view value) { - if (value.empty() || value.size() > static_cast(std::numeric_limits::max())) { - return {}; - } - - const int utf8_length = WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast(value.size()), - nullptr, 0, nullptr, nullptr); - if (utf8_length <= 0) { - return {}; - } - - std::string utf8_value(static_cast(utf8_length), '\0'); - WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast(value.size()), utf8_value.data(), utf8_length, - nullptr, nullptr); - return utf8_value; -} -#endif - -std::filesystem::path Utf8Path(const char* path) { -#ifdef _WIN32 - return std::filesystem::path{Utf8ToWideString(path)}; -#else - return std::filesystem::path{path}; -#endif -} - -std::string PathToUtf8String(const std::filesystem::path& path) { -#ifdef _WIN32 - return WideToUtf8String(path.wstring()); -#else - return path.string(); -#endif -} - -OrtStatus* ResolveEpContextDataPath(const OrtApi& api, const char* file_name, const OrtGraph* graph, - std::filesystem::path& data_path) { - data_path = Utf8Path(file_name); - if (data_path.is_absolute() || graph == nullptr) { - return nullptr; - } - - const ORTCHAR_T* model_path = nullptr; - RETURN_IF_ERROR(api.Graph_GetModelPath(graph, &model_path)); - if (model_path == nullptr || model_path[0] == 0) { - return nullptr; - } - - data_path = std::filesystem::path{model_path}.parent_path() / data_path; - return nullptr; -} - -OrtStatus* ReadEpContextDataFromFile(const OrtApi& api, const char* file_name, const OrtGraph* graph, - std::vector& data) { - std::filesystem::path data_path; - RETURN_IF_ERROR(ResolveEpContextDataPath(api, file_name, graph, data_path)); - std::ifstream input_stream(data_path, std::ios::binary); - if (!input_stream) { - const std::string message = "Failed to open EPContext data file for read: " + - PathToUtf8String(data_path); - return api.CreateStatus(ORT_FAIL, message.c_str()); - } - - data.assign(std::istreambuf_iterator{input_stream}, std::istreambuf_iterator{}); - if (input_stream.bad()) { - const std::string message = "Failed to read EPContext data file: " + - PathToUtf8String(data_path); - return api.CreateStatus(ORT_FAIL, message.c_str()); - } - - return nullptr; -} - -OrtStatus* WriteEpContextDataToFile(const OrtApi& api, const char* file_name, const OrtGraph* graph, - const void* buffer, size_t buffer_size) { - std::filesystem::path data_path; - RETURN_IF_ERROR(ResolveEpContextDataPath(api, file_name, graph, data_path)); - std::ofstream output_stream(data_path, std::ios::binary); - if (!output_stream) { - const std::string message = "Failed to open EPContext data file for write: " + - PathToUtf8String(data_path); - return api.CreateStatus(ORT_FAIL, message.c_str()); - } - - if (buffer_size != 0) { - if (buffer_size > static_cast(std::numeric_limits::max())) { - return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data buffer is too large to write"); - } - - output_stream.write(static_cast(buffer), static_cast(buffer_size)); - if (!output_stream) { - const std::string message = "Failed to write EPContext data file: " + - PathToUtf8String(data_path); - return api.CreateStatus(ORT_FAIL, message.c_str()); - } - } - - return nullptr; -} - -} // namespace - const FloatInitializer* MulKernel::TryGetSavedInitializer(const std::string& name) const { auto iter = float_initializers.find(name); return iter != float_initializers.end() ? &iter->second : nullptr; @@ -552,36 +422,10 @@ OrtStatus* ORT_API_CALL ExampleEp::CompileImpl(_In_ OrtEp* this_ptr, _In_ const std::string ep_cache_context; RETURN_IF_ERROR(ep_cache_context_attr.GetValue(ep_cache_context)); - OrtReadEpContextDataFunc read_func = nullptr; - void* read_state = nullptr; - RETURN_IF_ERROR(ep->ep_api.EpContextConfig_GetEpContextDataReadFunc(ep->ep_context_config_, &read_func, - &read_state)); - if (read_func != nullptr) { - Ort::AllocatorWithDefaultOptions allocator; - void* ep_context_data = nullptr; - size_t ep_context_data_size = 0; - OrtStatus* status = read_func(read_state, ep_cache_context.c_str(), allocator, &ep_context_data, - &ep_context_data_size); - if (status != nullptr) { - if (ep_context_data != nullptr) { - allocator.Free(ep_context_data); - } - return status; - } - - if (ep_context_data_size != 0 && ep_context_data == nullptr) { - return ep->ort_api.CreateStatus(ORT_FAIL, - "OrtReadEpContextDataFunc returned a null buffer for non-empty EPContext data"); - } - - if (ep_context_data != nullptr) { - allocator.Free(ep_context_data); - } - } else { - std::vector ep_context_data; - RETURN_IF_ERROR(ReadEpContextDataFromFile(ep->ort_api, ep_cache_context.c_str(), ort_graphs[0], - ep_context_data)); - } + std::vector ep_context_data; + RETURN_IF_ERROR(ep_context_data_utils::ReadEpContextDataWithFileFallback( + ep->ort_api, ep->ep_api, ep->ep_context_config_, ep_cache_context.c_str(), ort_graphs[0], + ep_context_data)); } // Create EpContextKernel for EPContext nodes - clearly separates from MulKernel @@ -695,16 +539,9 @@ OrtStatus* ExampleEp::CreateEpContextNodes(const OrtGraph* graph, std::string ep_ctx = config_.embed_ep_context_in_model ? "binary_data" : fused_node_name + ".ctx"; if (!config_.embed_ep_context_in_model) { const std::string ep_context_data = "binary_data"; - OrtWriteEpContextDataFunc write_func = nullptr; - void* write_state = nullptr; - RETURN_IF_ERROR(ep_api.EpContextConfig_GetEpContextDataWriteFunc(ep_context_config_, &write_func, - &write_state)); - if (write_func != nullptr) { - RETURN_IF_ERROR(write_func(write_state, ep_ctx.c_str(), ep_context_data.data(), ep_context_data.size())); - } else { - RETURN_IF_ERROR(WriteEpContextDataToFile(ort_api, ep_ctx.c_str(), graph, - ep_context_data.data(), ep_context_data.size())); - } + RETURN_IF_ERROR(ep_context_data_utils::WriteEpContextDataWithFileFallback( + ort_api, ep_api, ep_context_config_, ep_ctx.c_str(), graph, ep_context_data.data(), + ep_context_data.size())); } attributes[0] = Ort::OpAttr("ep_cache_context", ep_ctx.data(), static_cast(ep_ctx.size()), ORT_OP_ATTR_STRING); From 583dcbef3cf86dcff3ae1c919da43cf340e2189d Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Mon, 1 Jun 2026 22:26:54 -0700 Subject: [PATCH 08/48] Add EPContext helper fallback coverage --- .../autoep/library/ep_context_data_utils.h | 17 +- .../autoep/library/example_plugin_ep/ep.cc | 17 +- .../autoep/library/example_plugin_ep/ep.h | 1 + .../library/example_plugin_ep/ep_factory.cc | 4 + onnxruntime/test/autoep/test_execution.cc | 380 ++++++++++++++++++ 5 files changed, 415 insertions(+), 4 deletions(-) diff --git a/onnxruntime/test/autoep/library/ep_context_data_utils.h b/onnxruntime/test/autoep/library/ep_context_data_utils.h index b866973c0b48f..d5d3cac444e73 100644 --- a/onnxruntime/test/autoep/library/ep_context_data_utils.h +++ b/onnxruntime/test/autoep/library/ep_context_data_utils.h @@ -203,7 +203,8 @@ inline OrtStatus* ReadEpContextDataWithFileFallback(const OrtApi& api, const Ort inline OrtStatus* WriteEpContextDataWithFileFallback(const OrtApi& api, const OrtEpApi& ep_api, const OrtEpContextConfig* ep_context_config, - const char* file_name, const OrtGraph* graph, + const char* file_name, const char* fallback_file_name, + const OrtGraph* graph, const void* buffer, size_t buffer_size) { if (file_name == nullptr || file_name[0] == '\0') { return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); @@ -223,7 +224,19 @@ inline OrtStatus* WriteEpContextDataWithFileFallback(const OrtApi& api, const Or return write_func(write_state, file_name, buffer, buffer_size); } - return WriteEpContextDataToFile(api, file_name, graph, buffer, buffer_size); + if (fallback_file_name == nullptr || fallback_file_name[0] == '\0') { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data fallback file name must not be empty"); + } + + return WriteEpContextDataToFile(api, fallback_file_name, graph, buffer, buffer_size); +} + +inline OrtStatus* WriteEpContextDataWithFileFallback(const OrtApi& api, const OrtEpApi& ep_api, + const OrtEpContextConfig* ep_context_config, + const char* file_name, const OrtGraph* graph, + const void* buffer, size_t buffer_size) { + return WriteEpContextDataWithFileFallback(api, ep_api, ep_context_config, file_name, file_name, graph, buffer, + buffer_size); } } // namespace ep_context_data_utils diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc index 6818e85e698c7..2db576048f15d 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -539,9 +540,21 @@ OrtStatus* ExampleEp::CreateEpContextNodes(const OrtGraph* graph, std::string ep_ctx = config_.embed_ep_context_in_model ? "binary_data" : fused_node_name + ".ctx"; if (!config_.embed_ep_context_in_model) { const std::string ep_context_data = "binary_data"; + std::string fallback_ep_ctx = ep_ctx; + const OrtGraph* fallback_graph = graph; + if (!config_.ep_context_output_model_path.empty()) { + const std::filesystem::path output_model_path = + ep_context_data_utils::Utf8Path(config_.ep_context_output_model_path.c_str()); + const std::filesystem::path output_model_dir = output_model_path.parent_path(); + if (!output_model_dir.empty()) { + fallback_ep_ctx = ep_context_data_utils::PathToUtf8String( + output_model_dir / ep_context_data_utils::Utf8Path(ep_ctx.c_str())); + } + fallback_graph = nullptr; + } RETURN_IF_ERROR(ep_context_data_utils::WriteEpContextDataWithFileFallback( - ort_api, ep_api, ep_context_config_, ep_ctx.c_str(), graph, ep_context_data.data(), - ep_context_data.size())); + ort_api, ep_api, ep_context_config_, ep_ctx.c_str(), fallback_ep_ctx.c_str(), fallback_graph, + ep_context_data.data(), ep_context_data.size())); } attributes[0] = Ort::OpAttr("ep_cache_context", ep_ctx.data(), static_cast(ep_ctx.size()), ORT_OP_ATTR_STRING); diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep.h b/onnxruntime/test/autoep/library/example_plugin_ep/ep.h index b20ae8fe10825..a003b84d0fb9c 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep.h +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep.h @@ -63,6 +63,7 @@ class ExampleEp : public OrtEp, public ApiPtrs { bool enable_ep_context = false; bool embed_ep_context_in_model = true; bool enable_weightless_ep_context_nodes = false; + std::string ep_context_output_model_path; // Other EP configs (typically extracted from OrtSessionOptions or OrtHardwareDevice(s)) }; diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc index a72a9c2b4221c..f381a9adfb14b 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc @@ -220,17 +220,21 @@ OrtStatus* ORT_API_CALL ExampleEpFactory::CreateEpImpl(OrtEpFactory* this_ptr, // Note: should not store a direct reference to the session options object as its lifespan is not guaranteed. std::string ep_context_enable; std::string ep_context_embed_mode; + std::string ep_context_output_model_path; std::string weightless_ep_context_nodes_enable; RETURN_IF_ERROR(GetSessionConfigEntryOrDefault(*session_options, kOrtSessionOptionEpContextEnable, "0", ep_context_enable)); RETURN_IF_ERROR(GetSessionConfigEntryOrDefault(*session_options, kOrtSessionOptionEpContextEmbedMode, "1", ep_context_embed_mode)); + RETURN_IF_ERROR(GetSessionConfigEntryOrDefault(*session_options, kOrtSessionOptionEpContextFilePath, "", + ep_context_output_model_path)); RETURN_IF_ERROR(GetSessionConfigEntryOrDefault(*session_options, kOrtSessionOptionEpEnableWeightlessEpContextNodes, "0", weightless_ep_context_nodes_enable)); ExampleEp::Config config = {}; config.enable_ep_context = ep_context_enable == "1"; config.embed_ep_context_in_model = ep_context_embed_mode != "0"; + config.ep_context_output_model_path = std::move(ep_context_output_model_path); config.enable_weightless_ep_context_nodes = weightless_ep_context_nodes_enable == "1"; OrtEpContextConfig* ep_context_config_raw = nullptr; diff --git a/onnxruntime/test/autoep/test_execution.cc b/onnxruntime/test/autoep/test_execution.cc index 35cbdc9009acc..c51a3033fd054 100644 --- a/onnxruntime/test/autoep/test_execution.cc +++ b/onnxruntime/test/autoep/test_execution.cc @@ -19,6 +19,7 @@ #include "nlohmann/json.hpp" #include "test/autoep/test_autoep_utils.h" +#include "test/autoep/library/ep_context_data_utils.h" #include "test/autoep/library/example_plugin_ep/ep_test_hooks.h" #include "test/shared_lib/utils.h" #include "test/util/include/api_asserts.h" @@ -72,6 +73,95 @@ OrtStatus* ORT_API_CALL LoadEpContextDataCallback(void* state, const char* file_ return nullptr; } +OrtStatus* ORT_API_CALL LoadInvalidEpContextDataCallback(void* state, const char* file_name, + OrtAllocator* /*allocator*/, void** buffer, + size_t* data_size) { + auto* callback_state = static_cast(state); + callback_state->read_called = true; + callback_state->read_file_name = file_name; + + *buffer = nullptr; + *data_size = 1; + return nullptr; +} + +void ExpectOrtStatusError(OrtStatus* status_ptr, OrtErrorCode expected_code, std::string_view expected_message) { + Ort::Status status{status_ptr}; + ASSERT_FALSE(status.IsOK()); + EXPECT_EQ(status.GetErrorCode(), expected_code); + EXPECT_THAT(std::string{status.GetErrorMessage()}, ::testing::HasSubstr(std::string{expected_message})); +} + +std::filesystem::path PrepareTempTestDir(std::string_view name) { + std::filesystem::path test_dir = std::filesystem::temp_directory_path() / std::string{name}; + std::filesystem::remove_all(test_dir); + std::filesystem::create_directories(test_dir); + return test_dir; +} + +struct FakeEpContextConfigCallbacks { + OrtReadEpContextDataFunc read_func = nullptr; + void* read_state = nullptr; + OrtWriteEpContextDataFunc write_func = nullptr; + void* write_state = nullptr; +}; + +const OrtEpContextConfig* FakeEpContextConfig(FakeEpContextConfigCallbacks& callbacks) { + return reinterpret_cast(&callbacks); +} + +OrtStatus* ORT_API_CALL FakeEpContextConfigGetReadFunc(const OrtEpContextConfig* config, + OrtReadEpContextDataFunc* read_func, void** state) noexcept { + auto* callbacks = reinterpret_cast(config); + *read_func = callbacks->read_func; + *state = callbacks->read_state; + return nullptr; +} + +OrtStatus* ORT_API_CALL FakeEpContextConfigGetWriteFunc(const OrtEpContextConfig* config, + OrtWriteEpContextDataFunc* write_func, void** state) noexcept { + auto* callbacks = reinterpret_cast(config); + *write_func = callbacks->write_func; + *state = callbacks->write_state; + return nullptr; +} + +OrtEpApi MakeFakeEpApi() { + OrtEpApi ep_api{}; + ep_api.EpContextConfig_GetEpContextDataReadFunc = FakeEpContextConfigGetReadFunc; + ep_api.EpContextConfig_GetEpContextDataWriteFunc = FakeEpContextConfigGetWriteFunc; + return ep_api; +} + +void LoadModelProtoFromFile(const ORTCHAR_T* model_file, ONNX_NAMESPACE::ModelProto& model_proto) { + std::ifstream model_stream{std::filesystem::path(model_file), std::ios::binary}; + ASSERT_TRUE(model_stream.is_open()); + ASSERT_TRUE(model_proto.ParseFromIstream(&model_stream)); +} + +std::vector GetEpContextNodes(const ONNX_NAMESPACE::ModelProto& model_proto) { + std::vector ep_context_nodes; + + for (const auto& node : model_proto.graph().node()) { + if (node.domain() == kMSDomain && node.op_type() == "EPContext") { + ep_context_nodes.push_back(&node); + } + } + + return ep_context_nodes; +} + +const ONNX_NAMESPACE::AttributeProto* GetNodeAttribute(const ONNX_NAMESPACE::NodeProto& node, + std::string_view attribute_name) { + for (const auto& attribute : node.attribute()) { + if (attribute.name() == attribute_name) { + return &attribute; + } + } + + return nullptr; +} + void RunMulModelWithPluginEp(const ORTCHAR_T* model_path, const Ort::SessionOptions& session_options) { Ort::Session session(*ort_env, model_path, session_options); @@ -533,6 +623,161 @@ TEST(OrtEpLibrary, PluginEp_AppendV2_Fp16HardSigmoid_EpGraphAssignmentInfo) { ASSERT_EQ(ep_nodes[0].GetName(), std::string("hardsigmoid_0")); } +TEST(OrtEpLibrary, EpContextDataUtils_PathHelpersRoundTrip) { + const std::string file_name = "context_data.bin"; + +#ifdef _WIN32 + const std::wstring wide_file_name = ep_context_data_utils::Utf8ToWideString(file_name); + ASSERT_FALSE(wide_file_name.empty()); + EXPECT_EQ(ep_context_data_utils::WideToUtf8String(wide_file_name), file_name); + + const std::string invalid_utf8(1, static_cast(0xff)); + EXPECT_TRUE(ep_context_data_utils::Utf8ToWideString(invalid_utf8).empty()); +#endif + + const std::filesystem::path file_path = ep_context_data_utils::Utf8Path(file_name.c_str()); + ASSERT_FALSE(file_path.empty()); + EXPECT_EQ(ep_context_data_utils::PathToUtf8String(file_path), file_name); +} + +TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathAndInvalidArguments) { + const auto& api = Ort::GetApi(); + const auto fake_ep_api = MakeFakeEpApi(); + std::filesystem::path data_path; + + ExpectOrtStatusError(ep_context_data_utils::ResolveEpContextDataPath(api, nullptr, nullptr, data_path), + ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); + ExpectOrtStatusError(ep_context_data_utils::ResolveEpContextDataPath(api, "", nullptr, data_path), + ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); + + ASSERT_ORTSTATUS_OK(ep_context_data_utils::ResolveEpContextDataPath(api, "relative.ctx", nullptr, data_path)); + EXPECT_EQ(ep_context_data_utils::PathToUtf8String(data_path), "relative.ctx"); + + ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataToFile(api, "unused.ctx", nullptr, nullptr, 1), + ORT_INVALID_ARGUMENT, "EPContext data buffer must not be null for non-empty data"); + ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback(api, fake_ep_api, nullptr, + "unused.ctx", nullptr, nullptr, 1), + ORT_INVALID_ARGUMENT, "EPContext data buffer must not be null for non-empty data"); + + std::vector data; + ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataWithFileFallback(api, fake_ep_api, nullptr, "", nullptr, + data), + ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); + ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback(api, fake_ep_api, nullptr, "", nullptr, + nullptr, 0), + ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); + ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback( + api, fake_ep_api, nullptr, "logical_context_data.bin", "", nullptr, nullptr, 0), + ORT_INVALID_ARGUMENT, "EPContext data fallback file name must not be empty"); +} + +TEST(OrtEpLibrary, EpContextDataUtils_FileFallbackReadsAndWrites) { + const auto& api = Ort::GetApi(); + const auto fake_ep_api = MakeFakeEpApi(); + const std::filesystem::path test_dir = PrepareTempTestDir("ort_ep_context_data_utils_file_fallback_test"); + auto cleanup = gsl::finally([&]() { std::filesystem::remove_all(test_dir); }); + + const std::string payload = "file fallback payload"; + const std::filesystem::path data_path = test_dir / "context_data.bin"; + const std::string data_file_name = ep_context_data_utils::PathToUtf8String(data_path); + + ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataToFile(api, data_file_name.c_str(), nullptr, + payload.data(), payload.size())); + + std::vector data; + ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataFromFile(api, data_file_name.c_str(), nullptr, data)); + EXPECT_EQ(std::string(data.begin(), data.end()), payload); + + const std::filesystem::path wrapper_data_path = test_dir / "wrapper_context_data.bin"; + const std::string wrapper_data_file_name = ep_context_data_utils::PathToUtf8String(wrapper_data_path); + ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( + api, fake_ep_api, nullptr, wrapper_data_file_name.c_str(), nullptr, payload.data(), payload.size())); + + data.clear(); + ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataWithFileFallback( + api, fake_ep_api, nullptr, wrapper_data_file_name.c_str(), nullptr, data)); + EXPECT_EQ(std::string(data.begin(), data.end()), payload); + + const std::filesystem::path fallback_data_path = test_dir / "fallback_context_data.bin"; + const std::string fallback_data_file_name = ep_context_data_utils::PathToUtf8String(fallback_data_path); + ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( + api, fake_ep_api, nullptr, "logical_context_data.bin", fallback_data_file_name.c_str(), nullptr, + payload.data(), payload.size())); + + data.clear(); + ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataFromFile(api, fallback_data_file_name.c_str(), nullptr, + data)); + EXPECT_EQ(std::string(data.begin(), data.end()), payload); + + const std::filesystem::path empty_data_path = test_dir / "empty_context_data.bin"; + const std::string empty_data_file_name = ep_context_data_utils::PathToUtf8String(empty_data_path); + ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( + api, fake_ep_api, nullptr, empty_data_file_name.c_str(), nullptr, nullptr, 0)); + + data.assign({'s', 't', 'a', 'l', 'e'}); + ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataWithFileFallback( + api, fake_ep_api, nullptr, empty_data_file_name.c_str(), nullptr, data)); + EXPECT_TRUE(data.empty()); + + const std::filesystem::path missing_data_path = test_dir / "missing_context_data.bin"; + const std::string missing_data_file_name = ep_context_data_utils::PathToUtf8String(missing_data_path); + ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataFromFile(api, missing_data_file_name.c_str(), nullptr, + data), + ORT_FAIL, "Failed to open EPContext data file for read"); +} + +TEST(OrtEpLibrary, EpContextDataUtils_CallbackFallbackUsesCallbacks) { + const auto& api = Ort::GetApi(); + const auto fake_ep_api = MakeFakeEpApi(); + + EpContextDataCallbackState read_callback_state; + read_callback_state.payload = {'c', 'a', 'l', 'l', 'b', 'a', 'c', 'k'}; + EpContextDataCallbackState write_callback_state; + FakeEpContextConfigCallbacks callbacks{LoadEpContextDataCallback, &read_callback_state, + StoreEpContextDataCallback, &write_callback_state}; + + std::vector data; + ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataWithFileFallback( + api, fake_ep_api, FakeEpContextConfig(callbacks), "callback_context.bin", nullptr, data)); + ASSERT_TRUE(read_callback_state.read_called); + EXPECT_EQ(read_callback_state.read_file_name, "callback_context.bin"); + EXPECT_EQ(data, read_callback_state.payload); + + const std::string payload = "callback write payload"; + ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( + api, fake_ep_api, FakeEpContextConfig(callbacks), "callback_write_context.bin", nullptr, + payload.data(), payload.size())); + ASSERT_TRUE(write_callback_state.write_called); + EXPECT_EQ(write_callback_state.write_file_name, "callback_write_context.bin"); + EXPECT_EQ(std::string(write_callback_state.payload.begin(), write_callback_state.payload.end()), payload); + + write_callback_state = {}; + const std::string payload_with_unused_fallback = "callback write payload with unused fallback"; + ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( + api, fake_ep_api, FakeEpContextConfig(callbacks), "callback_write_context_unused_fallback.bin", "", nullptr, + payload_with_unused_fallback.data(), payload_with_unused_fallback.size())); + ASSERT_TRUE(write_callback_state.write_called); + EXPECT_EQ(write_callback_state.write_file_name, "callback_write_context_unused_fallback.bin"); + EXPECT_EQ(std::string(write_callback_state.payload.begin(), write_callback_state.payload.end()), + payload_with_unused_fallback); +} + +TEST(OrtEpLibrary, EpContextDataUtils_ReadCallbackRejectsNullBufferForNonEmptyPayload) { + const auto& api = Ort::GetApi(); + const auto fake_ep_api = MakeFakeEpApi(); + + EpContextDataCallbackState read_callback_state; + FakeEpContextConfigCallbacks callbacks{LoadInvalidEpContextDataCallback, &read_callback_state, nullptr, nullptr}; + + std::vector data; + ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataWithFileFallback( + api, fake_ep_api, FakeEpContextConfig(callbacks), "invalid_callback_context.bin", nullptr, + data), + ORT_FAIL, "OrtReadEpContextDataFunc returned a null buffer for non-empty EPContext data"); + ASSERT_TRUE(read_callback_state.read_called); + EXPECT_EQ(read_callback_state.read_file_name, "invalid_callback_context.bin"); +} + // Generate an EPContext model with a plugin EP. // This test uses the OrtCompileApi but could also be done by setting the appropriate session option configs. TEST(OrtEpLibrary, PluginEp_GenEpContextModel) { @@ -564,6 +809,141 @@ TEST(OrtEpLibrary, PluginEp_GenEpContextModel) { } } +TEST(OrtEpLibrary, PluginEp_GenEpContextModel_EmbedModeDoesNotUseCallbacks) { + RegisteredEpDeviceUniquePtr example_ep; + ASSERT_NO_FATAL_FAILURE(Utils::RegisterAndGetExampleEp(*ort_env, Utils::example_ep_info, example_ep)); + Ort::ConstEpDevice plugin_ep_device(example_ep.get()); + + const ORTCHAR_T* input_model_file = ORT_TSTR("testdata/mul_1.onnx"); + const ORTCHAR_T* output_model_file = ORT_TSTR("plugin_ep_mul_1_embedded_ctx.onnx"); + std::filesystem::remove(output_model_file); + auto cleanup = gsl::finally([&]() { std::filesystem::remove(output_model_file); }); + + EpContextDataCallbackState write_callback_state; + EpContextDataCallbackState compile_read_callback_state; + { + Ort::SessionOptions session_options; + session_options.SetEpContextDataReadFunc(LoadEpContextDataCallback, &compile_read_callback_state); + + std::unordered_map ep_options; + session_options.AppendExecutionProvider_V2(*ort_env, {plugin_ep_device}, ep_options); + + Ort::ModelCompilationOptions compile_options(*ort_env, session_options); + compile_options.SetFlags(OrtCompileApiFlags_ERROR_IF_NO_NODES_COMPILED); + compile_options.SetInputModelPath(input_model_file); + compile_options.SetOutputModelPath(output_model_file); + compile_options.SetEpContextEmbedMode(true); + compile_options.SetEpContextDataWriteFunc(StoreEpContextDataCallback, &write_callback_state); + + ASSERT_CXX_ORTSTATUS_OK(Ort::CompileModel(*ort_env, compile_options)); + } + + ASSERT_TRUE(std::filesystem::exists(output_model_file)); + EXPECT_FALSE(write_callback_state.write_called); + EXPECT_FALSE(compile_read_callback_state.read_called); + + ONNX_NAMESPACE::ModelProto compiled_model; + ASSERT_NO_FATAL_FAILURE(LoadModelProtoFromFile(output_model_file, compiled_model)); + + auto ep_context_nodes = GetEpContextNodes(compiled_model); + ASSERT_EQ(ep_context_nodes.size(), 1u); + + const ONNX_NAMESPACE::AttributeProto* embed_mode_attr = GetNodeAttribute(*ep_context_nodes[0], "embed_mode"); + ASSERT_NE(embed_mode_attr, nullptr); + EXPECT_EQ(embed_mode_attr->type(), ONNX_NAMESPACE::AttributeProto_AttributeType_INT); + EXPECT_EQ(embed_mode_attr->i(), 1); + + const ONNX_NAMESPACE::AttributeProto* ep_cache_context_attr = GetNodeAttribute(*ep_context_nodes[0], + "ep_cache_context"); + ASSERT_NE(ep_cache_context_attr, nullptr); + EXPECT_EQ(ep_cache_context_attr->type(), ONNX_NAMESPACE::AttributeProto_AttributeType_STRING); + EXPECT_EQ(ep_cache_context_attr->s(), "binary_data"); + + EpContextDataCallbackState load_read_callback_state; + { + Ort::SessionOptions session_options; + session_options.SetEpContextDataReadFunc(LoadEpContextDataCallback, &load_read_callback_state); + + std::unordered_map ep_options; + session_options.AppendExecutionProvider_V2(*ort_env, {plugin_ep_device}, ep_options); + + Ort::Session session(*ort_env, output_model_file, session_options); + } + + EXPECT_FALSE(load_read_callback_state.read_called); +} + +TEST(OrtEpLibrary, PluginEp_GenAndLoadEpContextModel_ExternalDataUsesFileFallback) { + RegisteredEpDeviceUniquePtr example_ep; + ASSERT_NO_FATAL_FAILURE(Utils::RegisterAndGetExampleEp(*ort_env, Utils::example_ep_info, example_ep)); + Ort::ConstEpDevice plugin_ep_device(example_ep.get()); + + const ORTCHAR_T* input_model_file = ORT_TSTR("testdata/mul_1.onnx"); + const ORTCHAR_T* output_model_file = ORT_TSTR("plugin_ep_mul_1_file_ctx.onnx"); + std::vector files_to_cleanup{std::filesystem::path{output_model_file}}; + for (const auto& path : files_to_cleanup) { + std::filesystem::remove(path); + } + auto cleanup = gsl::finally([&]() { + for (const auto& path : files_to_cleanup) { + std::filesystem::remove(path); + } + }); + + { + Ort::SessionOptions session_options; + std::unordered_map ep_options; + session_options.AppendExecutionProvider_V2(*ort_env, {plugin_ep_device}, ep_options); + + Ort::ModelCompilationOptions compile_options(*ort_env, session_options); + compile_options.SetFlags(OrtCompileApiFlags_ERROR_IF_NO_NODES_COMPILED); + compile_options.SetInputModelPath(input_model_file); + compile_options.SetOutputModelPath(output_model_file); + compile_options.SetEpContextEmbedMode(false); + + ASSERT_CXX_ORTSTATUS_OK(Ort::CompileModel(*ort_env, compile_options)); + } + + ASSERT_TRUE(std::filesystem::exists(output_model_file)); + + ONNX_NAMESPACE::ModelProto compiled_model; + ASSERT_NO_FATAL_FAILURE(LoadModelProtoFromFile(output_model_file, compiled_model)); + + auto ep_context_nodes = GetEpContextNodes(compiled_model); + ASSERT_EQ(ep_context_nodes.size(), 1u); + + const ONNX_NAMESPACE::AttributeProto* embed_mode_attr = GetNodeAttribute(*ep_context_nodes[0], "embed_mode"); + ASSERT_NE(embed_mode_attr, nullptr); + EXPECT_EQ(embed_mode_attr->type(), ONNX_NAMESPACE::AttributeProto_AttributeType_INT); + EXPECT_EQ(embed_mode_attr->i(), 0); + + const ONNX_NAMESPACE::AttributeProto* ep_cache_context_attr = GetNodeAttribute(*ep_context_nodes[0], + "ep_cache_context"); + ASSERT_NE(ep_cache_context_attr, nullptr); + EXPECT_EQ(ep_cache_context_attr->type(), ONNX_NAMESPACE::AttributeProto_AttributeType_STRING); + ASSERT_FALSE(ep_cache_context_attr->s().empty()); + + const std::filesystem::path output_model_dir = std::filesystem::path{output_model_file}.parent_path(); + const std::filesystem::path context_data_path = output_model_dir / + ep_context_data_utils::Utf8Path(ep_cache_context_attr->s().c_str()); + files_to_cleanup.push_back(context_data_path); + ASSERT_TRUE(std::filesystem::exists(context_data_path)); + + std::vector context_data; + const std::string context_data_file_name = ep_context_data_utils::PathToUtf8String(context_data_path); + ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataFromFile(Ort::GetApi(), context_data_file_name.c_str(), + nullptr, context_data)); + EXPECT_EQ(std::string(context_data.begin(), context_data.end()), "binary_data"); + + { + Ort::SessionOptions session_options; + std::unordered_map ep_options; + session_options.AppendExecutionProvider_V2(*ort_env, {plugin_ep_device}, ep_options); + + Ort::Session session(*ort_env, output_model_file, session_options); + } +} + TEST(OrtEpLibrary, PluginEp_GenEpContextModel_ExternalDataUsesWriteCallback) { RegisteredEpDeviceUniquePtr example_ep; ASSERT_NO_FATAL_FAILURE(Utils::RegisterAndGetExampleEp(*ort_env, Utils::example_ep_info, example_ep)); From 9693266292eddd9e064dea32c1da97cb66933806 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Tue, 2 Jun 2026 10:13:41 -0700 Subject: [PATCH 09/48] Address lintrunner review suggestions --- onnxruntime/test/autoep/test_execution.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/onnxruntime/test/autoep/test_execution.cc b/onnxruntime/test/autoep/test_execution.cc index c51a3033fd054..c9ce8aebdf67d 100644 --- a/onnxruntime/test/autoep/test_execution.cc +++ b/onnxruntime/test/autoep/test_execution.cc @@ -854,7 +854,7 @@ TEST(OrtEpLibrary, PluginEp_GenEpContextModel_EmbedModeDoesNotUseCallbacks) { EXPECT_EQ(embed_mode_attr->i(), 1); const ONNX_NAMESPACE::AttributeProto* ep_cache_context_attr = GetNodeAttribute(*ep_context_nodes[0], - "ep_cache_context"); + "ep_cache_context"); ASSERT_NE(ep_cache_context_attr, nullptr); EXPECT_EQ(ep_cache_context_attr->type(), ONNX_NAMESPACE::AttributeProto_AttributeType_STRING); EXPECT_EQ(ep_cache_context_attr->s(), "binary_data"); @@ -918,14 +918,14 @@ TEST(OrtEpLibrary, PluginEp_GenAndLoadEpContextModel_ExternalDataUsesFileFallbac EXPECT_EQ(embed_mode_attr->i(), 0); const ONNX_NAMESPACE::AttributeProto* ep_cache_context_attr = GetNodeAttribute(*ep_context_nodes[0], - "ep_cache_context"); + "ep_cache_context"); ASSERT_NE(ep_cache_context_attr, nullptr); EXPECT_EQ(ep_cache_context_attr->type(), ONNX_NAMESPACE::AttributeProto_AttributeType_STRING); ASSERT_FALSE(ep_cache_context_attr->s().empty()); const std::filesystem::path output_model_dir = std::filesystem::path{output_model_file}.parent_path(); const std::filesystem::path context_data_path = output_model_dir / - ep_context_data_utils::Utf8Path(ep_cache_context_attr->s().c_str()); + ep_context_data_utils::Utf8Path(ep_cache_context_attr->s().c_str()); files_to_cleanup.push_back(context_data_path); ASSERT_TRUE(std::filesystem::exists(context_data_path)); From 43deeb375d855c6ef8d913eadd64943eb5f93d67 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Tue, 2 Jun 2026 11:29:38 -0700 Subject: [PATCH 10/48] Address EPContext review comments --- onnxruntime/core/session/compile_api.cc | 4 ++-- onnxruntime/core/session/plugin_ep/ep_api.cc | 4 ++-- onnxruntime/test/autoep/library/ep_context_data_utils.h | 4 ++++ onnxruntime/test/autoep/library/example_plugin_ep/ep.cc | 1 + onnxruntime/test/autoep/test_execution.cc | 2 ++ 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/onnxruntime/core/session/compile_api.cc b/onnxruntime/core/session/compile_api.cc index 901f1754784a5..74cdeab375bb8 100644 --- a/onnxruntime/core/session/compile_api.cc +++ b/onnxruntime/core/session/compile_api.cc @@ -395,9 +395,9 @@ static constexpr OrtCompileApi ort_compile_api = { // End of Version 24 - DO NOT MODIFY ABOVE // End of Version 25 - DO NOT MODIFY ABOVE // End of Version 26 - DO NOT MODIFY ABOVE - // End of Version 27 - DO NOT MODIFY ABOVE &OrtCompileAPI::ModelCompilationOptions_SetEpContextDataWriteFunc, + // End of Version 27 - DO NOT MODIFY ABOVE }; // checks that we don't violate the rule that the functions must remain in the slots they were originally assigned @@ -407,7 +407,7 @@ static_assert(offsetof(OrtCompileApi, ModelCompilationOptions_SetOutputModelGetI "Size of version 23 of Api cannot change"); static_assert(offsetof(OrtCompileApi, ModelCompilationOptions_SetInputModel) / sizeof(void*) == 14, "Size of version 24 of Api cannot change"); -static_assert(offsetof(OrtCompileApi, ModelCompilationOptions_SetInputModel) / sizeof(void*) == 14, +static_assert(offsetof(OrtCompileApi, ModelCompilationOptions_SetEpContextDataWriteFunc) / sizeof(void*) == 15, "Size of version 27 of Api cannot change"); ORT_API(const OrtCompileApi*, OrtCompileAPI::GetCompileApi) { diff --git a/onnxruntime/core/session/plugin_ep/ep_api.cc b/onnxruntime/core/session/plugin_ep/ep_api.cc index 0ecdb2579eb18..b93fa98c04c8b 100644 --- a/onnxruntime/core/session/plugin_ep/ep_api.cc +++ b/onnxruntime/core/session/plugin_ep/ep_api.cc @@ -1352,12 +1352,12 @@ static constexpr OrtEpApi ort_ep_api = { &OrtExecutionProviderApi::ProfilingEventsContainer_AddEvents, // End of Version 25 - DO NOT MODIFY ABOVE // End of Version 26 - DO NOT MODIFY ABOVE - // End of Version 27 - DO NOT MODIFY ABOVE &OrtExecutionProviderApi::SessionOptions_GetEpContextConfig, &OrtExecutionProviderApi::ReleaseEpContextConfig, &OrtExecutionProviderApi::EpContextConfig_GetEpContextDataReadFunc, &OrtExecutionProviderApi::EpContextConfig_GetEpContextDataWriteFunc, + // End of Version 27 - DO NOT MODIFY ABOVE }; // checks that we don't violate the rule that the functions must remain in the slots they were originally assigned @@ -1369,7 +1369,7 @@ static_assert(offsetof(OrtEpApi, GetEnvConfigEntries) / sizeof(void*) == 49, "Size of version 24 API cannot change"); static_assert(offsetof(OrtEpApi, ProfilingEventsContainer_AddEvents) / sizeof(void*) == 72, "Size of version 25 API cannot change"); -static_assert(offsetof(OrtEpApi, ProfilingEventsContainer_AddEvents) / sizeof(void*) == 72, +static_assert(offsetof(OrtEpApi, EpContextConfig_GetEpContextDataWriteFunc) / sizeof(void*) == 76, "Size of version 27 API cannot change"); } // namespace OrtExecutionProviderApi diff --git a/onnxruntime/test/autoep/library/ep_context_data_utils.h b/onnxruntime/test/autoep/library/ep_context_data_utils.h index d5d3cac444e73..728861c07f544 100644 --- a/onnxruntime/test/autoep/library/ep_context_data_utils.h +++ b/onnxruntime/test/autoep/library/ep_context_data_utils.h @@ -58,6 +58,10 @@ inline std::string WideToUtf8String(std::wstring_view value) { #endif inline std::filesystem::path Utf8Path(const char* path) { + if (path == nullptr || path[0] == '\0') { + return {}; + } + #ifdef _WIN32 return std::filesystem::path{Utf8ToWideString(path)}; #else diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc index 2db576048f15d..2458f311b1f48 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc @@ -427,6 +427,7 @@ OrtStatus* ORT_API_CALL ExampleEp::CompileImpl(_In_ OrtEp* this_ptr, _In_ const RETURN_IF_ERROR(ep_context_data_utils::ReadEpContextDataWithFileFallback( ep->ort_api, ep->ep_api, ep->ep_context_config_, ep_cache_context.c_str(), ort_graphs[0], ep_context_data)); + (void)ep_context_data; } // Create EpContextKernel for EPContext nodes - clearly separates from MulKernel diff --git a/onnxruntime/test/autoep/test_execution.cc b/onnxruntime/test/autoep/test_execution.cc index c9ce8aebdf67d..6e3e4ec7d2de0 100644 --- a/onnxruntime/test/autoep/test_execution.cc +++ b/onnxruntime/test/autoep/test_execution.cc @@ -638,6 +638,8 @@ TEST(OrtEpLibrary, EpContextDataUtils_PathHelpersRoundTrip) { const std::filesystem::path file_path = ep_context_data_utils::Utf8Path(file_name.c_str()); ASSERT_FALSE(file_path.empty()); EXPECT_EQ(ep_context_data_utils::PathToUtf8String(file_path), file_name); + EXPECT_TRUE(ep_context_data_utils::Utf8Path(nullptr).empty()); + EXPECT_TRUE(ep_context_data_utils::Utf8Path("").empty()); } TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathAndInvalidArguments) { From cc2e3d3a697080da6496df384034115844d53c6c Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Tue, 2 Jun 2026 12:07:45 -0700 Subject: [PATCH 11/48] Address remaining EPContext review comments --- onnxruntime/test/autoep/library/ep_context_data_utils.h | 2 ++ onnxruntime/test/autoep/test_execution.cc | 5 +++++ onnxruntime/test/framework/ep_plugin_provider_test.cc | 1 - 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/onnxruntime/test/autoep/library/ep_context_data_utils.h b/onnxruntime/test/autoep/library/ep_context_data_utils.h index 728861c07f544..89f8ea7569a90 100644 --- a/onnxruntime/test/autoep/library/ep_context_data_utils.h +++ b/onnxruntime/test/autoep/library/ep_context_data_utils.h @@ -79,6 +79,8 @@ inline std::string PathToUtf8String(const std::filesystem::path& path) { inline OrtStatus* ResolveEpContextDataPath(const OrtApi& api, const char* file_name, const OrtGraph* graph, std::filesystem::path& data_path) { + data_path.clear(); + if (file_name == nullptr || file_name[0] == '\0') { return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); } diff --git a/onnxruntime/test/autoep/test_execution.cc b/onnxruntime/test/autoep/test_execution.cc index 6e3e4ec7d2de0..256c941acc541 100644 --- a/onnxruntime/test/autoep/test_execution.cc +++ b/onnxruntime/test/autoep/test_execution.cc @@ -647,10 +647,15 @@ TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathAndInvalidArguments) { const auto fake_ep_api = MakeFakeEpApi(); std::filesystem::path data_path; + data_path = "stale.ctx"; ExpectOrtStatusError(ep_context_data_utils::ResolveEpContextDataPath(api, nullptr, nullptr, data_path), ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); + EXPECT_TRUE(data_path.empty()); + + data_path = "stale.ctx"; ExpectOrtStatusError(ep_context_data_utils::ResolveEpContextDataPath(api, "", nullptr, data_path), ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); + EXPECT_TRUE(data_path.empty()); ASSERT_ORTSTATUS_OK(ep_context_data_utils::ResolveEpContextDataPath(api, "relative.ctx", nullptr, data_path)); EXPECT_EQ(ep_context_data_utils::PathToUtf8String(data_path), "relative.ctx"); diff --git a/onnxruntime/test/framework/ep_plugin_provider_test.cc b/onnxruntime/test/framework/ep_plugin_provider_test.cc index e186aed6bf427..ed9e8415d0616 100644 --- a/onnxruntime/test/framework/ep_plugin_provider_test.cc +++ b/onnxruntime/test/framework/ep_plugin_provider_test.cc @@ -20,7 +20,6 @@ #include "core/framework/op_kernel.h" #include "core/framework/resource_accountant.h" #include "core/graph/constants.h" -#include "core/graph/ep_api_types.h" #include "core/graph/graph_viewer.h" #include "core/graph/model.h" #include "core/optimizer/graph_optimizer_registry.h" From af32616fbb9176b2bf3808c642aa23c76b06862b Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Tue, 2 Jun 2026 12:28:03 -0700 Subject: [PATCH 12/48] Document EPContext API placement and sample path-safety --- include/onnxruntime/core/session/onnxruntime_c_api.h | 8 ++++++++ onnxruntime/test/autoep/library/ep_context_data_utils.h | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/include/onnxruntime/core/session/onnxruntime_c_api.h b/include/onnxruntime/core/session/onnxruntime_c_api.h index 20d604c6fc1f7..bb31c4d84f0ca 100644 --- a/include/onnxruntime/core/session/onnxruntime_c_api.h +++ b/include/onnxruntime/core/session/onnxruntime_c_api.h @@ -7572,6 +7572,10 @@ struct OrtApi { * When loading a compiled model with external (non-embedded) EPContext binary data, an execution provider can * retrieve this callback from OrtEpContextConfig and call it instead of reading the binary data from disk. * + * Reading happens at session load, so this callback is configured on OrtSessionOptions. The corresponding write + * callback runs only at compile time and is configured on OrtModelCompilationOptions via + * OrtCompileApi::ModelCompilationOptions_SetEpContextDataWriteFunc. + * * The state pointer is stored as-is and is not owned by ORT. It must remain valid while any session or EP created * from these options may call the callback. If the same state may be used by multiple EPs or threads, the application * is responsible for synchronization. @@ -8434,6 +8438,10 @@ struct OrtCompileApi { * When EPContext embed mode is disabled, execution providers can retrieve this callback from OrtEpContextConfig and * call it instead of writing EPContext binary data directly to disk. * + * Writing happens only at compile time, so this callback is configured on OrtModelCompilationOptions. The + * corresponding read callback runs at session load and is configured on OrtSessionOptions via + * OrtApi::SessionOptions_SetEpContextDataReadFunc. + * * The state pointer is stored as-is and is not owned by ORT. It must remain valid for the duration of the compile * operation that may call the callback. If the same state may be used by multiple EPs or threads, the application is * responsible for synchronization. diff --git a/onnxruntime/test/autoep/library/ep_context_data_utils.h b/onnxruntime/test/autoep/library/ep_context_data_utils.h index 89f8ea7569a90..8cfafc4e33a75 100644 --- a/onnxruntime/test/autoep/library/ep_context_data_utils.h +++ b/onnxruntime/test/autoep/library/ep_context_data_utils.h @@ -77,6 +77,11 @@ inline std::string PathToUtf8String(const std::filesystem::path& path) { #endif } +// NOTE: This sample resolves file_name (which can originate from an untrusted EPContext model's +// "ep_cache_context" attribute) directly into a filesystem path. An absolute path or one containing ".." +// segments can therefore escape the model directory. Production EPs that adopt this pattern should validate +// or sandbox the resolved path (e.g. reject absolute paths and traversal, confine to an allowed directory) +// and bound the amount of data read. inline OrtStatus* ResolveEpContextDataPath(const OrtApi& api, const char* file_name, const OrtGraph* graph, std::filesystem::path& data_path) { data_path.clear(); From 5aca0e17108d50b5041a412edc2abbbb955db43f Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Tue, 2 Jun 2026 18:13:22 -0700 Subject: [PATCH 13/48] Rename EPContext data callback typedefs --- .../core/session/onnxruntime_c_api.h | 66 ++++++++++--------- .../core/session/onnxruntime_cxx_api.h | 8 +-- .../core/session/onnxruntime_cxx_inline.h | 12 ++-- .../core/session/onnxruntime_ep_c_api.h | 8 +-- .../core/framework/ep_context_options.h | 2 +- onnxruntime/core/framework/session_options.h | 2 +- .../core/session/abi_session_options.cc | 2 +- onnxruntime/core/session/compile_api.cc | 4 +- onnxruntime/core/session/compile_api.h | 2 +- .../core/session/model_compilation_options.cc | 2 +- .../core/session/model_compilation_options.h | 4 +- onnxruntime/core/session/ort_apis.h | 2 +- onnxruntime/core/session/plugin_ep/ep_api.cc | 8 +-- onnxruntime/core/session/plugin_ep/ep_api.h | 4 +- .../autoep/library/ep_context_data_utils.h | 6 +- onnxruntime/test/autoep/test_execution.cc | 10 +-- .../test/framework/ep_plugin_provider_test.cc | 12 ++-- 17 files changed, 78 insertions(+), 76 deletions(-) diff --git a/include/onnxruntime/core/session/onnxruntime_c_api.h b/include/onnxruntime/core/session/onnxruntime_c_api.h index bb31c4d84f0ca..fd6fde32e1dd3 100644 --- a/include/onnxruntime/core/session/onnxruntime_c_api.h +++ b/include/onnxruntime/core/session/onnxruntime_c_api.h @@ -603,47 +603,49 @@ typedef OrtStatus*(ORT_API_CALL* OrtWriteBufferFunc)(_In_ void* state, _In_ const void* buffer, _In_ size_t buffer_num_bytes); -/** \brief Function called to write EPContext binary data during compilation. +/** \brief Function called to write named binary data. * - * This function is called synchronously by the execution provider on the calling thread. ORT does not own or retain - * buffer after the callback returns. ORT does not serialize invocations made by different EP instances or EP worker - * threads. + * This callback is currently used for EPContext binary data, but its contract is intentionally generic so future APIs + * can reuse it for other named data payloads. The callback is called synchronously by the component that receives it. + * ORT does not own or retain buffer after the callback returns. ORT does not serialize invocations made by different + * EP instances or worker threads. * * Each callback invocation represents one complete write operation for file_name. The callback signature does not - * provide an offset, sequence number, or final-chunk marker, so EPs that need chunked streaming must define their own - * ordering and completion contract with the application. EPs should prefer a single callback invocation per EPContext - * binary unless chunking semantics are documented by that EP. + * provide an offset, sequence number, or final-chunk marker, so the component invoking the callback must define any + * chunked ordering and completion contract with the application. Current EPContext use should prefer a single callback + * invocation per EPContext binary unless chunking semantics are documented by the EP. * * The application's implementation can process the data in any way (e.g., encrypt and store, upload to cloud storage, * or compress) before persisting it. * * \param[in] state Opaque pointer holding the user's state. ORT does not own or manage this pointer. The application - * must keep it valid for the duration of any compile operation that may invoke this callback and must - * provide any synchronization required if it can be used concurrently. - * \param[in] file_name The intended EPContext binary file name as a null-terminated UTF-8 string. - * \param[in] buffer The buffer containing EPContext binary data to write. + * must keep it valid for the duration required by the API that accepted the callback and must provide + * any synchronization required if it can be used concurrently. + * \param[in] file_name The file name or logical data identifier as a null-terminated UTF-8 string. + * \param[in] buffer The buffer containing data to write. * \param[in] buffer_num_bytes The size of the buffer in bytes. * * \return OrtStatus* Write status. Return nullptr on success. * Use CreateStatus to provide error info with ORT_FAIL as the error code. * ORT will release the OrtStatus* if not null. */ -typedef OrtStatus*(ORT_API_CALL* OrtWriteEpContextDataFunc)(_In_ void* state, - _In_ const char* file_name, - _In_ const void* buffer, - _In_ size_t buffer_num_bytes); +typedef OrtStatus*(ORT_API_CALL* OrtWriteFileDataFunc)(_In_ void* state, + _In_ const char* file_name, + _In_ const void* buffer, + _In_ size_t buffer_num_bytes); -/** \brief Function called by ORT to read EPContext binary data during session load. +/** \brief Function called to read named binary data. * - * The application reads, processes (e.g., decrypts, decompresses, downloads), and returns the EPContext binary data. - * ORT provides an allocator so the application can allocate the output buffer directly. The callback is called - * synchronously by the execution provider on the calling thread. ORT does not serialize invocations made by different - * EP instances or EP worker threads. + * This callback is currently used for EPContext binary data, but its contract is intentionally generic so future APIs + * can reuse it for other named data payloads. The application reads, processes (e.g., decrypts, decompresses, + * downloads), and returns the requested data. ORT provides an allocator so the application can allocate the output + * buffer directly. The callback is called synchronously by the component that receives it. ORT does not serialize + * invocations made by different EP instances or worker threads. * * \param[in] state Opaque pointer holding the user's state. ORT does not own or manage this pointer. The application - * must keep it valid while any session or EP created from the associated OrtSessionOptions may invoke - * this callback and must provide any synchronization required if it can be used concurrently. - * \param[in] file_name The EPContext binary file name as a null-terminated UTF-8 string. + * must keep it valid for the duration required by the API that accepted the callback and must provide + * any synchronization required if it can be used concurrently. + * \param[in] file_name The file name or logical data identifier to read as a null-terminated UTF-8 string. * \param[in] allocator ORT-provided allocator. The application must use this to allocate the output buffer. * \param[out] buffer Set by the implementation to the allocated buffer containing the output data. * \param[out] data_size Set by the implementation to the size of the output data in bytes. @@ -652,11 +654,11 @@ typedef OrtStatus*(ORT_API_CALL* OrtWriteEpContextDataFunc)(_In_ void* state, * Use CreateStatus to provide error info with ORT_FAIL as the error code. * ORT will release the OrtStatus* if not null. */ -typedef OrtStatus*(ORT_API_CALL* OrtReadEpContextDataFunc)(_In_ void* state, - _In_ const char* file_name, - _In_ OrtAllocator* allocator, - _Outptr_ void** buffer, - _Out_ size_t* data_size); +typedef OrtStatus*(ORT_API_CALL* OrtReadFileDataFunc)(_In_ void* state, + _In_ const char* file_name, + _In_ OrtAllocator* allocator, + _Outptr_ void** buffer, + _Out_ size_t* data_size); /** \brief Function called by ORT to allow user to specify how an initializer should be saved, that is, either * written to an external file or stored within the model. ORT calls this function for every initializer when @@ -7581,7 +7583,7 @@ struct OrtApi { * is responsible for synchronization. * * \param[in] options The OrtSessionOptions instance. - * \param[in] read_func The OrtReadEpContextDataFunc callback. + * \param[in] read_func The OrtReadFileDataFunc callback. * \param[in] state Opaque state passed to read_func. Can be NULL. * * \snippet{doc} snippets.dox OrtStatus Return Value @@ -7589,7 +7591,7 @@ struct OrtApi { * \since Version 1.28. */ ORT_API2_STATUS(SessionOptions_SetEpContextDataReadFunc, _Inout_ OrtSessionOptions* options, - _In_ OrtReadEpContextDataFunc read_func, _In_opt_ void* state); + _In_ OrtReadFileDataFunc read_func, _In_opt_ void* state); }; /* @@ -8447,7 +8449,7 @@ struct OrtCompileApi { * responsible for synchronization. * * \param[in] model_compile_options The OrtModelCompilationOptions instance. - * \param[in] write_func The OrtWriteEpContextDataFunc called to write EPContext bytes. + * \param[in] write_func The OrtWriteFileDataFunc callback used to write EPContext bytes. * \param[in] state Opaque state passed to write_func. Can be NULL. * * \snippet{doc} snippets.dox OrtStatus Return Value @@ -8456,7 +8458,7 @@ struct OrtCompileApi { */ ORT_API2_STATUS(ModelCompilationOptions_SetEpContextDataWriteFunc, _In_ OrtModelCompilationOptions* model_compile_options, - _In_ OrtWriteEpContextDataFunc write_func, _In_opt_ void* state); + _In_ OrtWriteFileDataFunc write_func, _In_opt_ void* state); }; /** diff --git a/include/onnxruntime/core/session/onnxruntime_cxx_api.h b/include/onnxruntime/core/session/onnxruntime_cxx_api.h index c8b3cb200552d..235615c1ff8f1 100644 --- a/include/onnxruntime/core/session/onnxruntime_cxx_api.h +++ b/include/onnxruntime/core/session/onnxruntime_cxx_api.h @@ -1196,10 +1196,10 @@ struct EpContextConfig : detail::Base { explicit EpContextConfig(const OrtSessionOptions* session_options); /// \brief Wraps OrtEpApi::EpContextConfig_GetEpContextDataReadFunc - std::pair GetEpContextDataReadFunc() const; + std::pair GetEpContextDataReadFunc() const; /// \brief Wraps OrtEpApi::EpContextConfig_GetEpContextDataWriteFunc - std::pair GetEpContextDataWriteFunc() const; + std::pair GetEpContextDataWriteFunc() const; }; /** \brief Validate a compiled model's compatibility for one or more EP devices. @@ -1685,7 +1685,7 @@ struct SessionOptionsImpl : ConstSessionOptionsImpl { const std::vector& external_initializer_file_buffer_array, const std::vector& external_initializer_file_lengths); ///< Wraps OrtApi::AddExternalInitializersFromFilesInMemory - SessionOptionsImpl& SetEpContextDataReadFunc(OrtReadEpContextDataFunc read_func, void* state); ///< Wraps OrtApi::SessionOptions_SetEpContextDataReadFunc + SessionOptionsImpl& SetEpContextDataReadFunc(OrtReadFileDataFunc read_func, void* state); ///< Wraps OrtApi::SessionOptions_SetEpContextDataReadFunc SessionOptionsImpl& AppendExecutionProvider_CPU(int use_arena); ///< Wraps OrtApi::SessionOptionsAppendExecutionProvider_CPU SessionOptionsImpl& AppendExecutionProvider_CUDA(const OrtCUDAProviderOptions& provider_options); ///< Wraps OrtApi::SessionOptionsAppendExecutionProvider_CUDA @@ -1789,7 +1789,7 @@ struct ModelCompilationOptions : detail::Base { ModelCompilationOptions& SetOutputModelWriteFunc(OrtWriteBufferFunc write_func, void* state); ///< Wraps OrtCompileApi::ModelCompilationOptions_SetEpContextDataWriteFunc - ModelCompilationOptions& SetEpContextDataWriteFunc(OrtWriteEpContextDataFunc write_func, void* state); + ModelCompilationOptions& SetEpContextDataWriteFunc(OrtWriteFileDataFunc write_func, void* state); ModelCompilationOptions& SetEpContextBinaryInformation(const ORTCHAR_T* output_directory, const ORTCHAR_T* model_name); ///< Wraps OrtApi::ModelCompilationOptions_SetEpContextBinaryInformation diff --git a/include/onnxruntime/core/session/onnxruntime_cxx_inline.h b/include/onnxruntime/core/session/onnxruntime_cxx_inline.h index 3eb19179002e1..535673d63bb27 100644 --- a/include/onnxruntime/core/session/onnxruntime_cxx_inline.h +++ b/include/onnxruntime/core/session/onnxruntime_cxx_inline.h @@ -773,15 +773,15 @@ inline EpContextConfig::EpContextConfig(const OrtSessionOptions* session_options ThrowOnError(GetEpApi().SessionOptions_GetEpContextConfig(session_options, &p_)); } -inline std::pair EpContextConfig::GetEpContextDataReadFunc() const { - OrtReadEpContextDataFunc read_func = nullptr; +inline std::pair EpContextConfig::GetEpContextDataReadFunc() const { + OrtReadFileDataFunc read_func = nullptr; void* state = nullptr; ThrowOnError(GetEpApi().EpContextConfig_GetEpContextDataReadFunc(this->p_, &read_func, &state)); return {read_func, state}; } -inline std::pair EpContextConfig::GetEpContextDataWriteFunc() const { - OrtWriteEpContextDataFunc write_func = nullptr; +inline std::pair EpContextConfig::GetEpContextDataWriteFunc() const { + OrtWriteFileDataFunc write_func = nullptr; void* state = nullptr; ThrowOnError(GetEpApi().EpContextConfig_GetEpContextDataWriteFunc(this->p_, &write_func, &state)); return {write_func, state}; @@ -1354,7 +1354,7 @@ inline ModelCompilationOptions& ModelCompilationOptions::SetOutputModelWriteFunc } inline ModelCompilationOptions& ModelCompilationOptions::SetEpContextDataWriteFunc( - OrtWriteEpContextDataFunc write_func, void* state) { + OrtWriteFileDataFunc write_func, void* state) { Ort::ThrowOnError(GetCompileApi().ModelCompilationOptions_SetEpContextDataWriteFunc(this->p_, write_func, state)); return *this; } @@ -1599,7 +1599,7 @@ inline SessionOptionsImpl& SessionOptionsImpl::AddExternalInitializersFrom } template -inline SessionOptionsImpl& SessionOptionsImpl::SetEpContextDataReadFunc(OrtReadEpContextDataFunc read_func, +inline SessionOptionsImpl& SessionOptionsImpl::SetEpContextDataReadFunc(OrtReadFileDataFunc read_func, void* state) { ThrowOnError(GetApi().SessionOptions_SetEpContextDataReadFunc(this->p_, read_func, state)); return *this; diff --git a/include/onnxruntime/core/session/onnxruntime_ep_c_api.h b/include/onnxruntime/core/session/onnxruntime_ep_c_api.h index 08ecc5b70a97f..0b37b684fa220 100644 --- a/include/onnxruntime/core/session/onnxruntime_ep_c_api.h +++ b/include/onnxruntime/core/session/onnxruntime_ep_c_api.h @@ -2110,7 +2110,7 @@ struct OrtEpApi { /** \brief Get the application-provided EPContext data read callback. * - * Returns the OrtReadEpContextDataFunc and opaque state pointer registered via + * Returns the OrtReadFileDataFunc and opaque state pointer registered via * OrtApi::SessionOptions_SetEpContextDataReadFunc. If no callback was registered, *read_func and *state are set to * NULL. The EP is responsible for calling the callback when present and for using its own normal read path when no * callback is present. @@ -2125,12 +2125,12 @@ struct OrtEpApi { */ ORT_API2_STATUS(EpContextConfig_GetEpContextDataReadFunc, _In_ const OrtEpContextConfig* config, - _Out_ OrtReadEpContextDataFunc* read_func, + _Out_ OrtReadFileDataFunc* read_func, _Out_ void** state); /** \brief Get the application-provided EPContext data write callback. * - * Returns the OrtWriteEpContextDataFunc and opaque state pointer registered via + * Returns the OrtWriteFileDataFunc and opaque state pointer registered via * OrtCompileApi::ModelCompilationOptions_SetEpContextDataWriteFunc. If no callback was registered, *write_func and * *state are set to NULL. The EP is responsible for calling the callback when present and for using its own normal * write path when no callback is present. @@ -2145,7 +2145,7 @@ struct OrtEpApi { */ ORT_API2_STATUS(EpContextConfig_GetEpContextDataWriteFunc, _In_ const OrtEpContextConfig* config, - _Out_ OrtWriteEpContextDataFunc* write_func, + _Out_ OrtWriteFileDataFunc* write_func, _Out_ void** state); }; diff --git a/onnxruntime/core/framework/ep_context_options.h b/onnxruntime/core/framework/ep_context_options.h index f05d0d95df73a..7cac9db6a054b 100644 --- a/onnxruntime/core/framework/ep_context_options.h +++ b/onnxruntime/core/framework/ep_context_options.h @@ -31,7 +31,7 @@ struct BufferWriteFuncHolder { /// Holds the opaque state and write function that EPs use to write EPContext binary data. /// struct EpContextDataWriteFuncHolder { - OrtWriteEpContextDataFunc write_func = nullptr; + OrtWriteFileDataFunc write_func = nullptr; void* state = nullptr; }; diff --git a/onnxruntime/core/framework/session_options.h b/onnxruntime/core/framework/session_options.h index ce406305cc17d..3c8ce8b2bf38c 100644 --- a/onnxruntime/core/framework/session_options.h +++ b/onnxruntime/core/framework/session_options.h @@ -227,7 +227,7 @@ struct SessionOptions { epctx::ModelGenOptions ep_context_gen_options = {}; epctx::ModelGenOptions GetEpContextGenerationOptions() const; - OrtReadEpContextDataFunc ep_context_data_read_func = nullptr; + OrtReadFileDataFunc ep_context_data_read_func = nullptr; void* ep_context_data_read_state = nullptr; }; diff --git a/onnxruntime/core/session/abi_session_options.cc b/onnxruntime/core/session/abi_session_options.cc index b72dc2c4afa37..a9df5390b6c68 100644 --- a/onnxruntime/core/session/abi_session_options.cc +++ b/onnxruntime/core/session/abi_session_options.cc @@ -135,7 +135,7 @@ ORT_API_STATUS_IMPL(OrtApis::GetSessionExecutionMode, _In_ const OrtSessionOptio } ORT_API_STATUS_IMPL(OrtApis::SessionOptions_SetEpContextDataReadFunc, _Inout_ OrtSessionOptions* options, - _In_ OrtReadEpContextDataFunc read_func, _In_opt_ void* state) { + _In_ OrtReadFileDataFunc read_func, _In_opt_ void* state) { API_IMPL_BEGIN ORT_API_RETURN_IF(options == nullptr, ORT_INVALID_ARGUMENT, "'options' parameter must not be NULL"); ORT_API_RETURN_IF(read_func == nullptr, ORT_INVALID_ARGUMENT, "'read_func' parameter must not be NULL"); diff --git a/onnxruntime/core/session/compile_api.cc b/onnxruntime/core/session/compile_api.cc index 74cdeab375bb8..c82a64da6edf7 100644 --- a/onnxruntime/core/session/compile_api.cc +++ b/onnxruntime/core/session/compile_api.cc @@ -261,7 +261,7 @@ ORT_API_STATUS_IMPL(OrtCompileAPI::ModelCompilationOptions_SetOutputModelGetInit ORT_API_STATUS_IMPL(OrtCompileAPI::ModelCompilationOptions_SetEpContextDataWriteFunc, _In_ OrtModelCompilationOptions* ort_model_compile_options, - _In_ OrtWriteEpContextDataFunc write_func, _In_opt_ void* state) { + _In_ OrtWriteFileDataFunc write_func, _In_opt_ void* state) { API_IMPL_BEGIN #if !defined(ORT_MINIMAL_BUILD) auto model_compile_options = reinterpret_cast(ort_model_compile_options); @@ -271,7 +271,7 @@ ORT_API_STATUS_IMPL(OrtCompileAPI::ModelCompilationOptions_SetEpContextDataWrite } if (write_func == nullptr) { - return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, "OrtWriteEpContextDataFunc function is null"); + return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, "OrtWriteFileDataFunc function is null"); } model_compile_options->SetEpContextDataWriteFunc(write_func, state); diff --git a/onnxruntime/core/session/compile_api.h b/onnxruntime/core/session/compile_api.h index 60c75bd5386a9..09689b20cda46 100644 --- a/onnxruntime/core/session/compile_api.h +++ b/onnxruntime/core/session/compile_api.h @@ -46,6 +46,6 @@ ORT_API_STATUS_IMPL(ModelCompilationOptions_SetInputModel, _In_ const OrtModel* model); ORT_API_STATUS_IMPL(ModelCompilationOptions_SetEpContextDataWriteFunc, _In_ OrtModelCompilationOptions* model_compile_options, - _In_ OrtWriteEpContextDataFunc write_func, _In_opt_ void* state); + _In_ OrtWriteFileDataFunc write_func, _In_opt_ void* state); } // namespace OrtCompileAPI diff --git a/onnxruntime/core/session/model_compilation_options.cc b/onnxruntime/core/session/model_compilation_options.cc index 607c175f70080..404a952cf6f66 100644 --- a/onnxruntime/core/session/model_compilation_options.cc +++ b/onnxruntime/core/session/model_compilation_options.cc @@ -133,7 +133,7 @@ void ModelCompilationOptions::SetOutputModelGetInitializerLocationFunc( }; } -void ModelCompilationOptions::SetEpContextDataWriteFunc(OrtWriteEpContextDataFunc write_func, void* state) { +void ModelCompilationOptions::SetEpContextDataWriteFunc(OrtWriteFileDataFunc write_func, void* state) { session_options_.value.ep_context_gen_options.ep_context_data_write_func = epctx::EpContextDataWriteFuncHolder{ write_func, state, diff --git a/onnxruntime/core/session/model_compilation_options.h b/onnxruntime/core/session/model_compilation_options.h index e24286df2b512..0db7fd6b53738 100644 --- a/onnxruntime/core/session/model_compilation_options.h +++ b/onnxruntime/core/session/model_compilation_options.h @@ -100,9 +100,9 @@ class ModelCompilationOptions { /// /// Sets a user-provided function to handle EPContext binary data writes. /// - /// The user-provided function called to write EPContext data + /// The user-provided OrtWriteFileDataFunc callback used to write EPContext data. /// The user's state. - void SetEpContextDataWriteFunc(OrtWriteEpContextDataFunc write_func, void* state); + void SetEpContextDataWriteFunc(OrtWriteFileDataFunc write_func, void* state); /// /// Sets information relate to EP context binary file. diff --git a/onnxruntime/core/session/ort_apis.h b/onnxruntime/core/session/ort_apis.h index 88fe7f1a09466..3b7f254fce9a7 100644 --- a/onnxruntime/core/session/ort_apis.h +++ b/onnxruntime/core/session/ort_apis.h @@ -67,7 +67,7 @@ ORT_API_STATUS_IMPL(DisableMemPattern, _In_ OrtSessionOptions* options); ORT_API_STATUS_IMPL(GetMemPatternEnabled, _In_ const OrtSessionOptions* options, _Out_ int* out); ORT_API_STATUS_IMPL(GetSessionExecutionMode, _In_ const OrtSessionOptions* options, _Out_ ExecutionMode* out); ORT_API_STATUS_IMPL(SessionOptions_SetEpContextDataReadFunc, _Inout_ OrtSessionOptions* options, - _In_ OrtReadEpContextDataFunc read_func, _In_opt_ void* state); + _In_ OrtReadFileDataFunc read_func, _In_opt_ void* state); ORT_API_STATUS_IMPL(EnableCpuMemArena, _In_ OrtSessionOptions* options); ORT_API_STATUS_IMPL(DisableCpuMemArena, _In_ OrtSessionOptions* options); ORT_API_STATUS_IMPL(SetSessionLogId, _In_ OrtSessionOptions* options, const char* logid); diff --git a/onnxruntime/core/session/plugin_ep/ep_api.cc b/onnxruntime/core/session/plugin_ep/ep_api.cc index b93fa98c04c8b..94d93ac0fb129 100644 --- a/onnxruntime/core/session/plugin_ep/ep_api.cc +++ b/onnxruntime/core/session/plugin_ep/ep_api.cc @@ -39,9 +39,9 @@ using namespace onnxruntime; struct OrtEpContextConfig { - OrtWriteEpContextDataFunc write_func = nullptr; + OrtWriteFileDataFunc write_func = nullptr; void* write_state = nullptr; - OrtReadEpContextDataFunc read_func = nullptr; + OrtReadFileDataFunc read_func = nullptr; void* read_state = nullptr; }; @@ -1234,7 +1234,7 @@ ORT_API(void, ReleaseEpContextConfig, _Frees_ptr_opt_ OrtEpContextConfig* config ORT_API_STATUS_IMPL(EpContextConfig_GetEpContextDataReadFunc, _In_ const OrtEpContextConfig* config, - _Out_ OrtReadEpContextDataFunc* read_func, + _Out_ OrtReadFileDataFunc* read_func, _Out_ void** state) { API_IMPL_BEGIN ORT_API_RETURN_IF(config == nullptr, ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); @@ -1249,7 +1249,7 @@ ORT_API_STATUS_IMPL(EpContextConfig_GetEpContextDataReadFunc, ORT_API_STATUS_IMPL(EpContextConfig_GetEpContextDataWriteFunc, _In_ const OrtEpContextConfig* config, - _Out_ OrtWriteEpContextDataFunc* write_func, + _Out_ OrtWriteFileDataFunc* write_func, _Out_ void** state) { API_IMPL_BEGIN ORT_API_RETURN_IF(config == nullptr, ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); diff --git a/onnxruntime/core/session/plugin_ep/ep_api.h b/onnxruntime/core/session/plugin_ep/ep_api.h index e3b5903f5f121..49772a00535cc 100644 --- a/onnxruntime/core/session/plugin_ep/ep_api.h +++ b/onnxruntime/core/session/plugin_ep/ep_api.h @@ -186,11 +186,11 @@ ORT_API_STATUS_IMPL(SessionOptions_GetEpContextConfig, ORT_API(void, ReleaseEpContextConfig, _Frees_ptr_opt_ OrtEpContextConfig* config); ORT_API_STATUS_IMPL(EpContextConfig_GetEpContextDataReadFunc, _In_ const OrtEpContextConfig* config, - _Out_ OrtReadEpContextDataFunc* read_func, + _Out_ OrtReadFileDataFunc* read_func, _Out_ void** state); ORT_API_STATUS_IMPL(EpContextConfig_GetEpContextDataWriteFunc, _In_ const OrtEpContextConfig* config, - _Out_ OrtWriteEpContextDataFunc* write_func, + _Out_ OrtWriteFileDataFunc* write_func, _Out_ void** state); } // namespace OrtExecutionProviderApi diff --git a/onnxruntime/test/autoep/library/ep_context_data_utils.h b/onnxruntime/test/autoep/library/ep_context_data_utils.h index 8cfafc4e33a75..593ab0b4c2008 100644 --- a/onnxruntime/test/autoep/library/ep_context_data_utils.h +++ b/onnxruntime/test/autoep/library/ep_context_data_utils.h @@ -173,7 +173,7 @@ inline OrtStatus* ReadEpContextDataWithFileFallback(const OrtApi& api, const Ort return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); } - OrtReadEpContextDataFunc read_func = nullptr; + OrtReadFileDataFunc read_func = nullptr; void* read_state = nullptr; if (ep_context_config != nullptr) { RETURN_IF_ERROR(ep_api.EpContextConfig_GetEpContextDataReadFunc(ep_context_config, &read_func, &read_state)); @@ -200,7 +200,7 @@ inline OrtStatus* ReadEpContextDataWithFileFallback(const OrtApi& api, const Ort if (ep_context_data_size != 0 && ep_context_data == nullptr) { return api.CreateStatus( - ORT_FAIL, "OrtReadEpContextDataFunc returned a null buffer for non-empty EPContext data"); + ORT_FAIL, "OrtReadFileDataFunc returned a null buffer for non-empty EPContext data"); } data.clear(); @@ -225,7 +225,7 @@ inline OrtStatus* WriteEpContextDataWithFileFallback(const OrtApi& api, const Or return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data buffer must not be null for non-empty data"); } - OrtWriteEpContextDataFunc write_func = nullptr; + OrtWriteFileDataFunc write_func = nullptr; void* write_state = nullptr; if (ep_context_config != nullptr) { RETURN_IF_ERROR(ep_api.EpContextConfig_GetEpContextDataWriteFunc(ep_context_config, &write_func, &write_state)); diff --git a/onnxruntime/test/autoep/test_execution.cc b/onnxruntime/test/autoep/test_execution.cc index 256c941acc541..63546841b2473 100644 --- a/onnxruntime/test/autoep/test_execution.cc +++ b/onnxruntime/test/autoep/test_execution.cc @@ -100,9 +100,9 @@ std::filesystem::path PrepareTempTestDir(std::string_view name) { } struct FakeEpContextConfigCallbacks { - OrtReadEpContextDataFunc read_func = nullptr; + OrtReadFileDataFunc read_func = nullptr; void* read_state = nullptr; - OrtWriteEpContextDataFunc write_func = nullptr; + OrtWriteFileDataFunc write_func = nullptr; void* write_state = nullptr; }; @@ -111,7 +111,7 @@ const OrtEpContextConfig* FakeEpContextConfig(FakeEpContextConfigCallbacks& call } OrtStatus* ORT_API_CALL FakeEpContextConfigGetReadFunc(const OrtEpContextConfig* config, - OrtReadEpContextDataFunc* read_func, void** state) noexcept { + OrtReadFileDataFunc* read_func, void** state) noexcept { auto* callbacks = reinterpret_cast(config); *read_func = callbacks->read_func; *state = callbacks->read_state; @@ -119,7 +119,7 @@ OrtStatus* ORT_API_CALL FakeEpContextConfigGetReadFunc(const OrtEpContextConfig* } OrtStatus* ORT_API_CALL FakeEpContextConfigGetWriteFunc(const OrtEpContextConfig* config, - OrtWriteEpContextDataFunc* write_func, void** state) noexcept { + OrtWriteFileDataFunc* write_func, void** state) noexcept { auto* callbacks = reinterpret_cast(config); *write_func = callbacks->write_func; *state = callbacks->write_state; @@ -780,7 +780,7 @@ TEST(OrtEpLibrary, EpContextDataUtils_ReadCallbackRejectsNullBufferForNonEmptyPa ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataWithFileFallback( api, fake_ep_api, FakeEpContextConfig(callbacks), "invalid_callback_context.bin", nullptr, data), - ORT_FAIL, "OrtReadEpContextDataFunc returned a null buffer for non-empty EPContext data"); + ORT_FAIL, "OrtReadFileDataFunc returned a null buffer for non-empty EPContext data"); ASSERT_TRUE(read_callback_state.read_called); EXPECT_EQ(read_callback_state.read_file_name, "invalid_callback_context.bin"); } diff --git a/onnxruntime/test/framework/ep_plugin_provider_test.cc b/onnxruntime/test/framework/ep_plugin_provider_test.cc index ed9e8415d0616..9b06e477a68ff 100644 --- a/onnxruntime/test/framework/ep_plugin_provider_test.cc +++ b/onnxruntime/test/framework/ep_plugin_provider_test.cc @@ -1789,7 +1789,7 @@ TEST(PluginExecutionProviderTest, EpContextDataReadFuncIsReturnedByEpApi) { ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(session_options, &ep_context_config)); auto release_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(ep_context_config); }); - OrtReadEpContextDataFunc read_func = nullptr; + OrtReadFileDataFunc read_func = nullptr; void* callback_state_out = nullptr; ASSERT_ORTSTATUS_OK(ep_api.EpContextConfig_GetEpContextDataReadFunc(ep_context_config, &read_func, &callback_state_out)); @@ -1832,8 +1832,8 @@ TEST(PluginExecutionProviderTest, EpContextDataApiRejectsInvalidArguments) { ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(session_options, &ep_context_config)); auto release_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(ep_context_config); }); - OrtReadEpContextDataFunc read_func = nullptr; - OrtWriteEpContextDataFunc write_func = nullptr; + OrtReadFileDataFunc read_func = nullptr; + OrtWriteFileDataFunc write_func = nullptr; void* state = nullptr; ExpectOrtStatus(ep_api.EpContextConfig_GetEpContextDataReadFunc(nullptr, &read_func, &state), ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); @@ -1857,7 +1857,7 @@ TEST(PluginExecutionProviderTest, EpContextDataApiRejectsInvalidArguments) { ORT_INVALID_ARGUMENT, "OrtModelCompilationOptions is NULL"); ExpectOrtStatus(compile_api.ModelCompilationOptions_SetEpContextDataWriteFunc(compilation_options, nullptr, nullptr), - ORT_INVALID_ARGUMENT, "OrtWriteEpContextDataFunc function is null"); + ORT_INVALID_ARGUMENT, "OrtWriteFileDataFunc function is null"); #endif // !defined(ORT_MINIMAL_BUILD) } @@ -1869,8 +1869,8 @@ TEST(PluginExecutionProviderTest, EpContextDataAccessorsReturnNullWhenCallbacksU ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(session_options, &ep_context_config)); auto release_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(ep_context_config); }); - OrtReadEpContextDataFunc read_func = EpContextReadCallback; - OrtWriteEpContextDataFunc write_func = EpContextWriteCallback; + OrtReadFileDataFunc read_func = EpContextReadCallback; + OrtWriteFileDataFunc write_func = EpContextWriteCallback; void* state = reinterpret_cast(0x1); ASSERT_ORTSTATUS_OK(ep_api.EpContextConfig_GetEpContextDataReadFunc(ep_context_config, &read_func, &state)); From cfcb96614adf74d7574af6764894ebda74b2b561 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Tue, 2 Jun 2026 18:50:57 -0700 Subject: [PATCH 14/48] Address review: default example EP embed mode to non-embedded; standardize NULL casing --- onnxruntime/core/session/compile_api.cc | 2 +- onnxruntime/test/autoep/library/example_plugin_ep/ep.h | 2 +- onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc | 2 +- onnxruntime/test/framework/ep_plugin_provider_test.cc | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/onnxruntime/core/session/compile_api.cc b/onnxruntime/core/session/compile_api.cc index c82a64da6edf7..7a2b5d3cb474a 100644 --- a/onnxruntime/core/session/compile_api.cc +++ b/onnxruntime/core/session/compile_api.cc @@ -271,7 +271,7 @@ ORT_API_STATUS_IMPL(OrtCompileAPI::ModelCompilationOptions_SetEpContextDataWrite } if (write_func == nullptr) { - return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, "OrtWriteFileDataFunc function is null"); + return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, "OrtWriteFileDataFunc function is NULL"); } model_compile_options->SetEpContextDataWriteFunc(write_func, state); diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep.h b/onnxruntime/test/autoep/library/example_plugin_ep/ep.h index a003b84d0fb9c..4f57bc04ce77e 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep.h +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep.h @@ -61,7 +61,7 @@ class ExampleEp : public OrtEp, public ApiPtrs { public: struct Config { bool enable_ep_context = false; - bool embed_ep_context_in_model = true; + bool embed_ep_context_in_model = false; bool enable_weightless_ep_context_nodes = false; std::string ep_context_output_model_path; // Other EP configs (typically extracted from OrtSessionOptions or OrtHardwareDevice(s)) diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc index f381a9adfb14b..3f7232db8f676 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc @@ -224,7 +224,7 @@ OrtStatus* ORT_API_CALL ExampleEpFactory::CreateEpImpl(OrtEpFactory* this_ptr, std::string weightless_ep_context_nodes_enable; RETURN_IF_ERROR(GetSessionConfigEntryOrDefault(*session_options, kOrtSessionOptionEpContextEnable, "0", ep_context_enable)); - RETURN_IF_ERROR(GetSessionConfigEntryOrDefault(*session_options, kOrtSessionOptionEpContextEmbedMode, "1", + RETURN_IF_ERROR(GetSessionConfigEntryOrDefault(*session_options, kOrtSessionOptionEpContextEmbedMode, "0", ep_context_embed_mode)); RETURN_IF_ERROR(GetSessionConfigEntryOrDefault(*session_options, kOrtSessionOptionEpContextFilePath, "", ep_context_output_model_path)); diff --git a/onnxruntime/test/framework/ep_plugin_provider_test.cc b/onnxruntime/test/framework/ep_plugin_provider_test.cc index 9b06e477a68ff..d98f1fb60c7da 100644 --- a/onnxruntime/test/framework/ep_plugin_provider_test.cc +++ b/onnxruntime/test/framework/ep_plugin_provider_test.cc @@ -1857,7 +1857,7 @@ TEST(PluginExecutionProviderTest, EpContextDataApiRejectsInvalidArguments) { ORT_INVALID_ARGUMENT, "OrtModelCompilationOptions is NULL"); ExpectOrtStatus(compile_api.ModelCompilationOptions_SetEpContextDataWriteFunc(compilation_options, nullptr, nullptr), - ORT_INVALID_ARGUMENT, "OrtWriteFileDataFunc function is null"); + ORT_INVALID_ARGUMENT, "OrtWriteFileDataFunc function is NULL"); #endif // !defined(ORT_MINIMAL_BUILD) } From 9ae612b2c83fc90c68e434aed6e136d835261413 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Tue, 2 Jun 2026 19:12:03 -0700 Subject: [PATCH 15/48] Fix ModelPackageTest.CheckCompiledModelCompatibilityInfo: embed EPContext data so packaged model is self-contained --- onnxruntime/test/autoep/test_model_package.cc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/onnxruntime/test/autoep/test_model_package.cc b/onnxruntime/test/autoep/test_model_package.cc index 34f73eb69b149..93b16f560a658 100644 --- a/onnxruntime/test/autoep/test_model_package.cc +++ b/onnxruntime/test/autoep/test_model_package.cc @@ -630,6 +630,11 @@ TEST(ModelPackageTest, CheckCompiledModelCompatibilityInfo) { compile_options.SetInputModelPath(input_model_file); compile_options.SetOutputModelPath(output_model_file); + // Embed the EPContext binary data inside the compiled model so the model is self-contained. + // This test copies only the compiled .onnx into the model package, so it must not rely on a + // separate sidecar EPContext data file (which non-embedded mode would produce). + compile_options.SetEpContextEmbedMode(true); + ASSERT_CXX_ORTSTATUS_OK(Ort::CompileModel(*ort_env, compile_options)); ASSERT_TRUE(std::filesystem::exists(output_model_file)); } From 9e8a41123518c8fb6008e8f11ce6a8fc4a1b88ce Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Wed, 3 Jun 2026 14:27:24 -0700 Subject: [PATCH 16/48] Rename OrtWriteFileDataFunc/OrtReadFileDataFunc to OrtWriteNamedBufferFunc/OrtReadNamedBufferFunc --- .../core/session/onnxruntime_c_api.h | 32 +++++++++---------- .../core/session/onnxruntime_cxx_api.h | 8 ++--- .../core/session/onnxruntime_cxx_inline.h | 12 +++---- .../core/session/onnxruntime_ep_c_api.h | 8 ++--- .../core/framework/ep_context_options.h | 2 +- onnxruntime/core/framework/session_options.h | 2 +- .../core/session/abi_session_options.cc | 2 +- onnxruntime/core/session/compile_api.cc | 4 +-- onnxruntime/core/session/compile_api.h | 2 +- .../core/session/model_compilation_options.cc | 2 +- .../core/session/model_compilation_options.h | 4 +-- onnxruntime/core/session/ort_apis.h | 2 +- onnxruntime/core/session/plugin_ep/ep_api.cc | 8 ++--- onnxruntime/core/session/plugin_ep/ep_api.h | 4 +-- .../autoep/library/ep_context_data_utils.h | 6 ++-- onnxruntime/test/autoep/test_execution.cc | 10 +++--- .../test/framework/ep_plugin_provider_test.cc | 12 +++---- 17 files changed, 60 insertions(+), 60 deletions(-) diff --git a/include/onnxruntime/core/session/onnxruntime_c_api.h b/include/onnxruntime/core/session/onnxruntime_c_api.h index fd6fde32e1dd3..5cfa291d5bd02 100644 --- a/include/onnxruntime/core/session/onnxruntime_c_api.h +++ b/include/onnxruntime/core/session/onnxruntime_c_api.h @@ -610,7 +610,7 @@ typedef OrtStatus*(ORT_API_CALL* OrtWriteBufferFunc)(_In_ void* state, * ORT does not own or retain buffer after the callback returns. ORT does not serialize invocations made by different * EP instances or worker threads. * - * Each callback invocation represents one complete write operation for file_name. The callback signature does not + * Each callback invocation represents one complete write operation for name. The callback signature does not * provide an offset, sequence number, or final-chunk marker, so the component invoking the callback must define any * chunked ordering and completion contract with the application. Current EPContext use should prefer a single callback * invocation per EPContext binary unless chunking semantics are documented by the EP. @@ -621,7 +621,7 @@ typedef OrtStatus*(ORT_API_CALL* OrtWriteBufferFunc)(_In_ void* state, * \param[in] state Opaque pointer holding the user's state. ORT does not own or manage this pointer. The application * must keep it valid for the duration required by the API that accepted the callback and must provide * any synchronization required if it can be used concurrently. - * \param[in] file_name The file name or logical data identifier as a null-terminated UTF-8 string. + * \param[in] name The file name or logical data identifier as a null-terminated UTF-8 string. * \param[in] buffer The buffer containing data to write. * \param[in] buffer_num_bytes The size of the buffer in bytes. * @@ -629,10 +629,10 @@ typedef OrtStatus*(ORT_API_CALL* OrtWriteBufferFunc)(_In_ void* state, * Use CreateStatus to provide error info with ORT_FAIL as the error code. * ORT will release the OrtStatus* if not null. */ -typedef OrtStatus*(ORT_API_CALL* OrtWriteFileDataFunc)(_In_ void* state, - _In_ const char* file_name, - _In_ const void* buffer, - _In_ size_t buffer_num_bytes); +typedef OrtStatus*(ORT_API_CALL* OrtWriteNamedBufferFunc)(_In_ void* state, + _In_ const char* name, + _In_ const void* buffer, + _In_ size_t buffer_num_bytes); /** \brief Function called to read named binary data. * @@ -645,7 +645,7 @@ typedef OrtStatus*(ORT_API_CALL* OrtWriteFileDataFunc)(_In_ void* state, * \param[in] state Opaque pointer holding the user's state. ORT does not own or manage this pointer. The application * must keep it valid for the duration required by the API that accepted the callback and must provide * any synchronization required if it can be used concurrently. - * \param[in] file_name The file name or logical data identifier to read as a null-terminated UTF-8 string. + * \param[in] name The file name or logical data identifier to read as a null-terminated UTF-8 string. * \param[in] allocator ORT-provided allocator. The application must use this to allocate the output buffer. * \param[out] buffer Set by the implementation to the allocated buffer containing the output data. * \param[out] data_size Set by the implementation to the size of the output data in bytes. @@ -654,11 +654,11 @@ typedef OrtStatus*(ORT_API_CALL* OrtWriteFileDataFunc)(_In_ void* state, * Use CreateStatus to provide error info with ORT_FAIL as the error code. * ORT will release the OrtStatus* if not null. */ -typedef OrtStatus*(ORT_API_CALL* OrtReadFileDataFunc)(_In_ void* state, - _In_ const char* file_name, - _In_ OrtAllocator* allocator, - _Outptr_ void** buffer, - _Out_ size_t* data_size); +typedef OrtStatus*(ORT_API_CALL* OrtReadNamedBufferFunc)(_In_ void* state, + _In_ const char* name, + _In_ OrtAllocator* allocator, + _Outptr_ void** buffer, + _Out_ size_t* data_size); /** \brief Function called by ORT to allow user to specify how an initializer should be saved, that is, either * written to an external file or stored within the model. ORT calls this function for every initializer when @@ -7583,7 +7583,7 @@ struct OrtApi { * is responsible for synchronization. * * \param[in] options The OrtSessionOptions instance. - * \param[in] read_func The OrtReadFileDataFunc callback. + * \param[in] read_func The OrtReadNamedBufferFunc callback. * \param[in] state Opaque state passed to read_func. Can be NULL. * * \snippet{doc} snippets.dox OrtStatus Return Value @@ -7591,7 +7591,7 @@ struct OrtApi { * \since Version 1.28. */ ORT_API2_STATUS(SessionOptions_SetEpContextDataReadFunc, _Inout_ OrtSessionOptions* options, - _In_ OrtReadFileDataFunc read_func, _In_opt_ void* state); + _In_ OrtReadNamedBufferFunc read_func, _In_opt_ void* state); }; /* @@ -8449,7 +8449,7 @@ struct OrtCompileApi { * responsible for synchronization. * * \param[in] model_compile_options The OrtModelCompilationOptions instance. - * \param[in] write_func The OrtWriteFileDataFunc callback used to write EPContext bytes. + * \param[in] write_func The OrtWriteNamedBufferFunc callback used to write EPContext bytes. * \param[in] state Opaque state passed to write_func. Can be NULL. * * \snippet{doc} snippets.dox OrtStatus Return Value @@ -8458,7 +8458,7 @@ struct OrtCompileApi { */ ORT_API2_STATUS(ModelCompilationOptions_SetEpContextDataWriteFunc, _In_ OrtModelCompilationOptions* model_compile_options, - _In_ OrtWriteFileDataFunc write_func, _In_opt_ void* state); + _In_ OrtWriteNamedBufferFunc write_func, _In_opt_ void* state); }; /** diff --git a/include/onnxruntime/core/session/onnxruntime_cxx_api.h b/include/onnxruntime/core/session/onnxruntime_cxx_api.h index 235615c1ff8f1..f5bc169ea1422 100644 --- a/include/onnxruntime/core/session/onnxruntime_cxx_api.h +++ b/include/onnxruntime/core/session/onnxruntime_cxx_api.h @@ -1196,10 +1196,10 @@ struct EpContextConfig : detail::Base { explicit EpContextConfig(const OrtSessionOptions* session_options); /// \brief Wraps OrtEpApi::EpContextConfig_GetEpContextDataReadFunc - std::pair GetEpContextDataReadFunc() const; + std::pair GetEpContextDataReadFunc() const; /// \brief Wraps OrtEpApi::EpContextConfig_GetEpContextDataWriteFunc - std::pair GetEpContextDataWriteFunc() const; + std::pair GetEpContextDataWriteFunc() const; }; /** \brief Validate a compiled model's compatibility for one or more EP devices. @@ -1685,7 +1685,7 @@ struct SessionOptionsImpl : ConstSessionOptionsImpl { const std::vector& external_initializer_file_buffer_array, const std::vector& external_initializer_file_lengths); ///< Wraps OrtApi::AddExternalInitializersFromFilesInMemory - SessionOptionsImpl& SetEpContextDataReadFunc(OrtReadFileDataFunc read_func, void* state); ///< Wraps OrtApi::SessionOptions_SetEpContextDataReadFunc + SessionOptionsImpl& SetEpContextDataReadFunc(OrtReadNamedBufferFunc read_func, void* state); ///< Wraps OrtApi::SessionOptions_SetEpContextDataReadFunc SessionOptionsImpl& AppendExecutionProvider_CPU(int use_arena); ///< Wraps OrtApi::SessionOptionsAppendExecutionProvider_CPU SessionOptionsImpl& AppendExecutionProvider_CUDA(const OrtCUDAProviderOptions& provider_options); ///< Wraps OrtApi::SessionOptionsAppendExecutionProvider_CUDA @@ -1789,7 +1789,7 @@ struct ModelCompilationOptions : detail::Base { ModelCompilationOptions& SetOutputModelWriteFunc(OrtWriteBufferFunc write_func, void* state); ///< Wraps OrtCompileApi::ModelCompilationOptions_SetEpContextDataWriteFunc - ModelCompilationOptions& SetEpContextDataWriteFunc(OrtWriteFileDataFunc write_func, void* state); + ModelCompilationOptions& SetEpContextDataWriteFunc(OrtWriteNamedBufferFunc write_func, void* state); ModelCompilationOptions& SetEpContextBinaryInformation(const ORTCHAR_T* output_directory, const ORTCHAR_T* model_name); ///< Wraps OrtApi::ModelCompilationOptions_SetEpContextBinaryInformation diff --git a/include/onnxruntime/core/session/onnxruntime_cxx_inline.h b/include/onnxruntime/core/session/onnxruntime_cxx_inline.h index 535673d63bb27..fe1478d789715 100644 --- a/include/onnxruntime/core/session/onnxruntime_cxx_inline.h +++ b/include/onnxruntime/core/session/onnxruntime_cxx_inline.h @@ -773,15 +773,15 @@ inline EpContextConfig::EpContextConfig(const OrtSessionOptions* session_options ThrowOnError(GetEpApi().SessionOptions_GetEpContextConfig(session_options, &p_)); } -inline std::pair EpContextConfig::GetEpContextDataReadFunc() const { - OrtReadFileDataFunc read_func = nullptr; +inline std::pair EpContextConfig::GetEpContextDataReadFunc() const { + OrtReadNamedBufferFunc read_func = nullptr; void* state = nullptr; ThrowOnError(GetEpApi().EpContextConfig_GetEpContextDataReadFunc(this->p_, &read_func, &state)); return {read_func, state}; } -inline std::pair EpContextConfig::GetEpContextDataWriteFunc() const { - OrtWriteFileDataFunc write_func = nullptr; +inline std::pair EpContextConfig::GetEpContextDataWriteFunc() const { + OrtWriteNamedBufferFunc write_func = nullptr; void* state = nullptr; ThrowOnError(GetEpApi().EpContextConfig_GetEpContextDataWriteFunc(this->p_, &write_func, &state)); return {write_func, state}; @@ -1354,7 +1354,7 @@ inline ModelCompilationOptions& ModelCompilationOptions::SetOutputModelWriteFunc } inline ModelCompilationOptions& ModelCompilationOptions::SetEpContextDataWriteFunc( - OrtWriteFileDataFunc write_func, void* state) { + OrtWriteNamedBufferFunc write_func, void* state) { Ort::ThrowOnError(GetCompileApi().ModelCompilationOptions_SetEpContextDataWriteFunc(this->p_, write_func, state)); return *this; } @@ -1599,7 +1599,7 @@ inline SessionOptionsImpl& SessionOptionsImpl::AddExternalInitializersFrom } template -inline SessionOptionsImpl& SessionOptionsImpl::SetEpContextDataReadFunc(OrtReadFileDataFunc read_func, +inline SessionOptionsImpl& SessionOptionsImpl::SetEpContextDataReadFunc(OrtReadNamedBufferFunc read_func, void* state) { ThrowOnError(GetApi().SessionOptions_SetEpContextDataReadFunc(this->p_, read_func, state)); return *this; diff --git a/include/onnxruntime/core/session/onnxruntime_ep_c_api.h b/include/onnxruntime/core/session/onnxruntime_ep_c_api.h index 0b37b684fa220..8243ce1c511b8 100644 --- a/include/onnxruntime/core/session/onnxruntime_ep_c_api.h +++ b/include/onnxruntime/core/session/onnxruntime_ep_c_api.h @@ -2110,7 +2110,7 @@ struct OrtEpApi { /** \brief Get the application-provided EPContext data read callback. * - * Returns the OrtReadFileDataFunc and opaque state pointer registered via + * Returns the OrtReadNamedBufferFunc and opaque state pointer registered via * OrtApi::SessionOptions_SetEpContextDataReadFunc. If no callback was registered, *read_func and *state are set to * NULL. The EP is responsible for calling the callback when present and for using its own normal read path when no * callback is present. @@ -2125,12 +2125,12 @@ struct OrtEpApi { */ ORT_API2_STATUS(EpContextConfig_GetEpContextDataReadFunc, _In_ const OrtEpContextConfig* config, - _Out_ OrtReadFileDataFunc* read_func, + _Out_ OrtReadNamedBufferFunc* read_func, _Out_ void** state); /** \brief Get the application-provided EPContext data write callback. * - * Returns the OrtWriteFileDataFunc and opaque state pointer registered via + * Returns the OrtWriteNamedBufferFunc and opaque state pointer registered via * OrtCompileApi::ModelCompilationOptions_SetEpContextDataWriteFunc. If no callback was registered, *write_func and * *state are set to NULL. The EP is responsible for calling the callback when present and for using its own normal * write path when no callback is present. @@ -2145,7 +2145,7 @@ struct OrtEpApi { */ ORT_API2_STATUS(EpContextConfig_GetEpContextDataWriteFunc, _In_ const OrtEpContextConfig* config, - _Out_ OrtWriteFileDataFunc* write_func, + _Out_ OrtWriteNamedBufferFunc* write_func, _Out_ void** state); }; diff --git a/onnxruntime/core/framework/ep_context_options.h b/onnxruntime/core/framework/ep_context_options.h index 7cac9db6a054b..aa9053b60438e 100644 --- a/onnxruntime/core/framework/ep_context_options.h +++ b/onnxruntime/core/framework/ep_context_options.h @@ -31,7 +31,7 @@ struct BufferWriteFuncHolder { /// Holds the opaque state and write function that EPs use to write EPContext binary data. /// struct EpContextDataWriteFuncHolder { - OrtWriteFileDataFunc write_func = nullptr; + OrtWriteNamedBufferFunc write_func = nullptr; void* state = nullptr; }; diff --git a/onnxruntime/core/framework/session_options.h b/onnxruntime/core/framework/session_options.h index 3c8ce8b2bf38c..5c8ba0e62061a 100644 --- a/onnxruntime/core/framework/session_options.h +++ b/onnxruntime/core/framework/session_options.h @@ -227,7 +227,7 @@ struct SessionOptions { epctx::ModelGenOptions ep_context_gen_options = {}; epctx::ModelGenOptions GetEpContextGenerationOptions() const; - OrtReadFileDataFunc ep_context_data_read_func = nullptr; + OrtReadNamedBufferFunc ep_context_data_read_func = nullptr; void* ep_context_data_read_state = nullptr; }; diff --git a/onnxruntime/core/session/abi_session_options.cc b/onnxruntime/core/session/abi_session_options.cc index a9df5390b6c68..f24bc52814a7d 100644 --- a/onnxruntime/core/session/abi_session_options.cc +++ b/onnxruntime/core/session/abi_session_options.cc @@ -135,7 +135,7 @@ ORT_API_STATUS_IMPL(OrtApis::GetSessionExecutionMode, _In_ const OrtSessionOptio } ORT_API_STATUS_IMPL(OrtApis::SessionOptions_SetEpContextDataReadFunc, _Inout_ OrtSessionOptions* options, - _In_ OrtReadFileDataFunc read_func, _In_opt_ void* state) { + _In_ OrtReadNamedBufferFunc read_func, _In_opt_ void* state) { API_IMPL_BEGIN ORT_API_RETURN_IF(options == nullptr, ORT_INVALID_ARGUMENT, "'options' parameter must not be NULL"); ORT_API_RETURN_IF(read_func == nullptr, ORT_INVALID_ARGUMENT, "'read_func' parameter must not be NULL"); diff --git a/onnxruntime/core/session/compile_api.cc b/onnxruntime/core/session/compile_api.cc index 7a2b5d3cb474a..819e31d6eb416 100644 --- a/onnxruntime/core/session/compile_api.cc +++ b/onnxruntime/core/session/compile_api.cc @@ -261,7 +261,7 @@ ORT_API_STATUS_IMPL(OrtCompileAPI::ModelCompilationOptions_SetOutputModelGetInit ORT_API_STATUS_IMPL(OrtCompileAPI::ModelCompilationOptions_SetEpContextDataWriteFunc, _In_ OrtModelCompilationOptions* ort_model_compile_options, - _In_ OrtWriteFileDataFunc write_func, _In_opt_ void* state) { + _In_ OrtWriteNamedBufferFunc write_func, _In_opt_ void* state) { API_IMPL_BEGIN #if !defined(ORT_MINIMAL_BUILD) auto model_compile_options = reinterpret_cast(ort_model_compile_options); @@ -271,7 +271,7 @@ ORT_API_STATUS_IMPL(OrtCompileAPI::ModelCompilationOptions_SetEpContextDataWrite } if (write_func == nullptr) { - return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, "OrtWriteFileDataFunc function is NULL"); + return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, "OrtWriteNamedBufferFunc function is NULL"); } model_compile_options->SetEpContextDataWriteFunc(write_func, state); diff --git a/onnxruntime/core/session/compile_api.h b/onnxruntime/core/session/compile_api.h index 09689b20cda46..c733153b9aeb5 100644 --- a/onnxruntime/core/session/compile_api.h +++ b/onnxruntime/core/session/compile_api.h @@ -46,6 +46,6 @@ ORT_API_STATUS_IMPL(ModelCompilationOptions_SetInputModel, _In_ const OrtModel* model); ORT_API_STATUS_IMPL(ModelCompilationOptions_SetEpContextDataWriteFunc, _In_ OrtModelCompilationOptions* model_compile_options, - _In_ OrtWriteFileDataFunc write_func, _In_opt_ void* state); + _In_ OrtWriteNamedBufferFunc write_func, _In_opt_ void* state); } // namespace OrtCompileAPI diff --git a/onnxruntime/core/session/model_compilation_options.cc b/onnxruntime/core/session/model_compilation_options.cc index 404a952cf6f66..32dd4a4a77e9c 100644 --- a/onnxruntime/core/session/model_compilation_options.cc +++ b/onnxruntime/core/session/model_compilation_options.cc @@ -133,7 +133,7 @@ void ModelCompilationOptions::SetOutputModelGetInitializerLocationFunc( }; } -void ModelCompilationOptions::SetEpContextDataWriteFunc(OrtWriteFileDataFunc write_func, void* state) { +void ModelCompilationOptions::SetEpContextDataWriteFunc(OrtWriteNamedBufferFunc write_func, void* state) { session_options_.value.ep_context_gen_options.ep_context_data_write_func = epctx::EpContextDataWriteFuncHolder{ write_func, state, diff --git a/onnxruntime/core/session/model_compilation_options.h b/onnxruntime/core/session/model_compilation_options.h index 0db7fd6b53738..c92f2362e2413 100644 --- a/onnxruntime/core/session/model_compilation_options.h +++ b/onnxruntime/core/session/model_compilation_options.h @@ -100,9 +100,9 @@ class ModelCompilationOptions { /// /// Sets a user-provided function to handle EPContext binary data writes. /// - /// The user-provided OrtWriteFileDataFunc callback used to write EPContext data. + /// The user-provided OrtWriteNamedBufferFunc callback used to write EPContext data. /// The user's state. - void SetEpContextDataWriteFunc(OrtWriteFileDataFunc write_func, void* state); + void SetEpContextDataWriteFunc(OrtWriteNamedBufferFunc write_func, void* state); /// /// Sets information relate to EP context binary file. diff --git a/onnxruntime/core/session/ort_apis.h b/onnxruntime/core/session/ort_apis.h index 3b7f254fce9a7..60cb8ed6674fb 100644 --- a/onnxruntime/core/session/ort_apis.h +++ b/onnxruntime/core/session/ort_apis.h @@ -67,7 +67,7 @@ ORT_API_STATUS_IMPL(DisableMemPattern, _In_ OrtSessionOptions* options); ORT_API_STATUS_IMPL(GetMemPatternEnabled, _In_ const OrtSessionOptions* options, _Out_ int* out); ORT_API_STATUS_IMPL(GetSessionExecutionMode, _In_ const OrtSessionOptions* options, _Out_ ExecutionMode* out); ORT_API_STATUS_IMPL(SessionOptions_SetEpContextDataReadFunc, _Inout_ OrtSessionOptions* options, - _In_ OrtReadFileDataFunc read_func, _In_opt_ void* state); + _In_ OrtReadNamedBufferFunc read_func, _In_opt_ void* state); ORT_API_STATUS_IMPL(EnableCpuMemArena, _In_ OrtSessionOptions* options); ORT_API_STATUS_IMPL(DisableCpuMemArena, _In_ OrtSessionOptions* options); ORT_API_STATUS_IMPL(SetSessionLogId, _In_ OrtSessionOptions* options, const char* logid); diff --git a/onnxruntime/core/session/plugin_ep/ep_api.cc b/onnxruntime/core/session/plugin_ep/ep_api.cc index 94d93ac0fb129..846c786c7f283 100644 --- a/onnxruntime/core/session/plugin_ep/ep_api.cc +++ b/onnxruntime/core/session/plugin_ep/ep_api.cc @@ -39,9 +39,9 @@ using namespace onnxruntime; struct OrtEpContextConfig { - OrtWriteFileDataFunc write_func = nullptr; + OrtWriteNamedBufferFunc write_func = nullptr; void* write_state = nullptr; - OrtReadFileDataFunc read_func = nullptr; + OrtReadNamedBufferFunc read_func = nullptr; void* read_state = nullptr; }; @@ -1234,7 +1234,7 @@ ORT_API(void, ReleaseEpContextConfig, _Frees_ptr_opt_ OrtEpContextConfig* config ORT_API_STATUS_IMPL(EpContextConfig_GetEpContextDataReadFunc, _In_ const OrtEpContextConfig* config, - _Out_ OrtReadFileDataFunc* read_func, + _Out_ OrtReadNamedBufferFunc* read_func, _Out_ void** state) { API_IMPL_BEGIN ORT_API_RETURN_IF(config == nullptr, ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); @@ -1249,7 +1249,7 @@ ORT_API_STATUS_IMPL(EpContextConfig_GetEpContextDataReadFunc, ORT_API_STATUS_IMPL(EpContextConfig_GetEpContextDataWriteFunc, _In_ const OrtEpContextConfig* config, - _Out_ OrtWriteFileDataFunc* write_func, + _Out_ OrtWriteNamedBufferFunc* write_func, _Out_ void** state) { API_IMPL_BEGIN ORT_API_RETURN_IF(config == nullptr, ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); diff --git a/onnxruntime/core/session/plugin_ep/ep_api.h b/onnxruntime/core/session/plugin_ep/ep_api.h index 49772a00535cc..63029f61a0948 100644 --- a/onnxruntime/core/session/plugin_ep/ep_api.h +++ b/onnxruntime/core/session/plugin_ep/ep_api.h @@ -186,11 +186,11 @@ ORT_API_STATUS_IMPL(SessionOptions_GetEpContextConfig, ORT_API(void, ReleaseEpContextConfig, _Frees_ptr_opt_ OrtEpContextConfig* config); ORT_API_STATUS_IMPL(EpContextConfig_GetEpContextDataReadFunc, _In_ const OrtEpContextConfig* config, - _Out_ OrtReadFileDataFunc* read_func, + _Out_ OrtReadNamedBufferFunc* read_func, _Out_ void** state); ORT_API_STATUS_IMPL(EpContextConfig_GetEpContextDataWriteFunc, _In_ const OrtEpContextConfig* config, - _Out_ OrtWriteFileDataFunc* write_func, + _Out_ OrtWriteNamedBufferFunc* write_func, _Out_ void** state); } // namespace OrtExecutionProviderApi diff --git a/onnxruntime/test/autoep/library/ep_context_data_utils.h b/onnxruntime/test/autoep/library/ep_context_data_utils.h index 593ab0b4c2008..ce31476ee3693 100644 --- a/onnxruntime/test/autoep/library/ep_context_data_utils.h +++ b/onnxruntime/test/autoep/library/ep_context_data_utils.h @@ -173,7 +173,7 @@ inline OrtStatus* ReadEpContextDataWithFileFallback(const OrtApi& api, const Ort return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); } - OrtReadFileDataFunc read_func = nullptr; + OrtReadNamedBufferFunc read_func = nullptr; void* read_state = nullptr; if (ep_context_config != nullptr) { RETURN_IF_ERROR(ep_api.EpContextConfig_GetEpContextDataReadFunc(ep_context_config, &read_func, &read_state)); @@ -200,7 +200,7 @@ inline OrtStatus* ReadEpContextDataWithFileFallback(const OrtApi& api, const Ort if (ep_context_data_size != 0 && ep_context_data == nullptr) { return api.CreateStatus( - ORT_FAIL, "OrtReadFileDataFunc returned a null buffer for non-empty EPContext data"); + ORT_FAIL, "OrtReadNamedBufferFunc returned a null buffer for non-empty EPContext data"); } data.clear(); @@ -225,7 +225,7 @@ inline OrtStatus* WriteEpContextDataWithFileFallback(const OrtApi& api, const Or return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data buffer must not be null for non-empty data"); } - OrtWriteFileDataFunc write_func = nullptr; + OrtWriteNamedBufferFunc write_func = nullptr; void* write_state = nullptr; if (ep_context_config != nullptr) { RETURN_IF_ERROR(ep_api.EpContextConfig_GetEpContextDataWriteFunc(ep_context_config, &write_func, &write_state)); diff --git a/onnxruntime/test/autoep/test_execution.cc b/onnxruntime/test/autoep/test_execution.cc index 63546841b2473..e56a027bb214a 100644 --- a/onnxruntime/test/autoep/test_execution.cc +++ b/onnxruntime/test/autoep/test_execution.cc @@ -100,9 +100,9 @@ std::filesystem::path PrepareTempTestDir(std::string_view name) { } struct FakeEpContextConfigCallbacks { - OrtReadFileDataFunc read_func = nullptr; + OrtReadNamedBufferFunc read_func = nullptr; void* read_state = nullptr; - OrtWriteFileDataFunc write_func = nullptr; + OrtWriteNamedBufferFunc write_func = nullptr; void* write_state = nullptr; }; @@ -111,7 +111,7 @@ const OrtEpContextConfig* FakeEpContextConfig(FakeEpContextConfigCallbacks& call } OrtStatus* ORT_API_CALL FakeEpContextConfigGetReadFunc(const OrtEpContextConfig* config, - OrtReadFileDataFunc* read_func, void** state) noexcept { + OrtReadNamedBufferFunc* read_func, void** state) noexcept { auto* callbacks = reinterpret_cast(config); *read_func = callbacks->read_func; *state = callbacks->read_state; @@ -119,7 +119,7 @@ OrtStatus* ORT_API_CALL FakeEpContextConfigGetReadFunc(const OrtEpContextConfig* } OrtStatus* ORT_API_CALL FakeEpContextConfigGetWriteFunc(const OrtEpContextConfig* config, - OrtWriteFileDataFunc* write_func, void** state) noexcept { + OrtWriteNamedBufferFunc* write_func, void** state) noexcept { auto* callbacks = reinterpret_cast(config); *write_func = callbacks->write_func; *state = callbacks->write_state; @@ -780,7 +780,7 @@ TEST(OrtEpLibrary, EpContextDataUtils_ReadCallbackRejectsNullBufferForNonEmptyPa ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataWithFileFallback( api, fake_ep_api, FakeEpContextConfig(callbacks), "invalid_callback_context.bin", nullptr, data), - ORT_FAIL, "OrtReadFileDataFunc returned a null buffer for non-empty EPContext data"); + ORT_FAIL, "OrtReadNamedBufferFunc returned a null buffer for non-empty EPContext data"); ASSERT_TRUE(read_callback_state.read_called); EXPECT_EQ(read_callback_state.read_file_name, "invalid_callback_context.bin"); } diff --git a/onnxruntime/test/framework/ep_plugin_provider_test.cc b/onnxruntime/test/framework/ep_plugin_provider_test.cc index d98f1fb60c7da..e702445ff5795 100644 --- a/onnxruntime/test/framework/ep_plugin_provider_test.cc +++ b/onnxruntime/test/framework/ep_plugin_provider_test.cc @@ -1789,7 +1789,7 @@ TEST(PluginExecutionProviderTest, EpContextDataReadFuncIsReturnedByEpApi) { ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(session_options, &ep_context_config)); auto release_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(ep_context_config); }); - OrtReadFileDataFunc read_func = nullptr; + OrtReadNamedBufferFunc read_func = nullptr; void* callback_state_out = nullptr; ASSERT_ORTSTATUS_OK(ep_api.EpContextConfig_GetEpContextDataReadFunc(ep_context_config, &read_func, &callback_state_out)); @@ -1832,8 +1832,8 @@ TEST(PluginExecutionProviderTest, EpContextDataApiRejectsInvalidArguments) { ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(session_options, &ep_context_config)); auto release_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(ep_context_config); }); - OrtReadFileDataFunc read_func = nullptr; - OrtWriteFileDataFunc write_func = nullptr; + OrtReadNamedBufferFunc read_func = nullptr; + OrtWriteNamedBufferFunc write_func = nullptr; void* state = nullptr; ExpectOrtStatus(ep_api.EpContextConfig_GetEpContextDataReadFunc(nullptr, &read_func, &state), ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); @@ -1857,7 +1857,7 @@ TEST(PluginExecutionProviderTest, EpContextDataApiRejectsInvalidArguments) { ORT_INVALID_ARGUMENT, "OrtModelCompilationOptions is NULL"); ExpectOrtStatus(compile_api.ModelCompilationOptions_SetEpContextDataWriteFunc(compilation_options, nullptr, nullptr), - ORT_INVALID_ARGUMENT, "OrtWriteFileDataFunc function is NULL"); + ORT_INVALID_ARGUMENT, "OrtWriteNamedBufferFunc function is NULL"); #endif // !defined(ORT_MINIMAL_BUILD) } @@ -1869,8 +1869,8 @@ TEST(PluginExecutionProviderTest, EpContextDataAccessorsReturnNullWhenCallbacksU ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(session_options, &ep_context_config)); auto release_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(ep_context_config); }); - OrtReadFileDataFunc read_func = EpContextReadCallback; - OrtWriteFileDataFunc write_func = EpContextWriteCallback; + OrtReadNamedBufferFunc read_func = EpContextReadCallback; + OrtWriteNamedBufferFunc write_func = EpContextWriteCallback; void* state = reinterpret_cast(0x1); ASSERT_ORTSTATUS_OK(ep_api.EpContextConfig_GetEpContextDataReadFunc(ep_context_config, &read_func, &state)); From 617f4182881dfbbd8fbe939596fbdd0949f24c27 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Wed, 10 Jun 2026 14:51:17 -0700 Subject: [PATCH 17/48] Make EPContext data callback APIs experimental Convert the six EPContext read/write callback C APIs from stable APIs to experimental APIs using the mechanism introduced in PR #28746: - OrtApi_SessionOptions_SetEpContextDataReadFunc - OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc - OrtEpApi_SessionOptions_GetEpContextConfig - OrtEpApi_ReleaseEpContextConfig - OrtEpApi_EpContextConfig_GetEpContextDataReadFunc - OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc Remove the C++ convenience wrappers and move the auxiliary types (OrtEpContextConfig, OrtReadNamedBufferFunc, OrtWriteNamedBufferFunc) into onnxruntime_experimental_c_api.h. Update the example plugin EP, sample helper utilities, and tests to use the generated experimental accessors. --- .../core/session/onnxruntime_c_api.h | 106 ------------ .../core/session/onnxruntime_cxx_api.h | 22 --- .../core/session/onnxruntime_cxx_inline.h | 31 ---- .../core/session/onnxruntime_ep_c_api.h | 70 -------- .../session/onnxruntime_experimental_c_api.h | 62 +++++++ .../onnxruntime_experimental_c_api.inc | 103 +++++++++++ .../core/framework/ep_context_options.h | 1 + onnxruntime/core/framework/session_options.h | 1 + .../core/session/abi_session_options.cc | 12 -- onnxruntime/core/session/compile_api.cc | 33 ---- onnxruntime/core/session/compile_api.h | 3 - .../core/session/experimental_c_api.cc | 109 ++++++++++++ .../core/session/model_compilation_options.h | 1 + onnxruntime/core/session/onnxruntime_c_api.cc | 1 - onnxruntime/core/session/ort_apis.h | 2 - onnxruntime/core/session/plugin_ep/ep_api.cc | 73 -------- onnxruntime/core/session/plugin_ep/ep_api.h | 14 -- .../autoep/library/ep_context_data_utils.h | 43 +++-- .../autoep/library/example_plugin_ep/ep.cc | 21 ++- .../autoep/library/example_plugin_ep/ep.h | 5 +- .../library/example_plugin_ep/ep_factory.cc | 10 +- onnxruntime/test/autoep/test_execution.cc | 77 +++++---- .../test/framework/ep_plugin_provider_test.cc | 161 +++++++++++++----- 23 files changed, 490 insertions(+), 471 deletions(-) diff --git a/include/onnxruntime/core/session/onnxruntime_c_api.h b/include/onnxruntime/core/session/onnxruntime_c_api.h index 5cfa291d5bd02..0289bca5c84d2 100644 --- a/include/onnxruntime/core/session/onnxruntime_c_api.h +++ b/include/onnxruntime/core/session/onnxruntime_c_api.h @@ -603,63 +603,6 @@ typedef OrtStatus*(ORT_API_CALL* OrtWriteBufferFunc)(_In_ void* state, _In_ const void* buffer, _In_ size_t buffer_num_bytes); -/** \brief Function called to write named binary data. - * - * This callback is currently used for EPContext binary data, but its contract is intentionally generic so future APIs - * can reuse it for other named data payloads. The callback is called synchronously by the component that receives it. - * ORT does not own or retain buffer after the callback returns. ORT does not serialize invocations made by different - * EP instances or worker threads. - * - * Each callback invocation represents one complete write operation for name. The callback signature does not - * provide an offset, sequence number, or final-chunk marker, so the component invoking the callback must define any - * chunked ordering and completion contract with the application. Current EPContext use should prefer a single callback - * invocation per EPContext binary unless chunking semantics are documented by the EP. - * - * The application's implementation can process the data in any way (e.g., encrypt and store, upload to cloud storage, - * or compress) before persisting it. - * - * \param[in] state Opaque pointer holding the user's state. ORT does not own or manage this pointer. The application - * must keep it valid for the duration required by the API that accepted the callback and must provide - * any synchronization required if it can be used concurrently. - * \param[in] name The file name or logical data identifier as a null-terminated UTF-8 string. - * \param[in] buffer The buffer containing data to write. - * \param[in] buffer_num_bytes The size of the buffer in bytes. - * - * \return OrtStatus* Write status. Return nullptr on success. - * Use CreateStatus to provide error info with ORT_FAIL as the error code. - * ORT will release the OrtStatus* if not null. - */ -typedef OrtStatus*(ORT_API_CALL* OrtWriteNamedBufferFunc)(_In_ void* state, - _In_ const char* name, - _In_ const void* buffer, - _In_ size_t buffer_num_bytes); - -/** \brief Function called to read named binary data. - * - * This callback is currently used for EPContext binary data, but its contract is intentionally generic so future APIs - * can reuse it for other named data payloads. The application reads, processes (e.g., decrypts, decompresses, - * downloads), and returns the requested data. ORT provides an allocator so the application can allocate the output - * buffer directly. The callback is called synchronously by the component that receives it. ORT does not serialize - * invocations made by different EP instances or worker threads. - * - * \param[in] state Opaque pointer holding the user's state. ORT does not own or manage this pointer. The application - * must keep it valid for the duration required by the API that accepted the callback and must provide - * any synchronization required if it can be used concurrently. - * \param[in] name The file name or logical data identifier to read as a null-terminated UTF-8 string. - * \param[in] allocator ORT-provided allocator. The application must use this to allocate the output buffer. - * \param[out] buffer Set by the implementation to the allocated buffer containing the output data. - * \param[out] data_size Set by the implementation to the size of the output data in bytes. - * - * \return OrtStatus* Read status. Return nullptr on success. - * Use CreateStatus to provide error info with ORT_FAIL as the error code. - * ORT will release the OrtStatus* if not null. - */ -typedef OrtStatus*(ORT_API_CALL* OrtReadNamedBufferFunc)(_In_ void* state, - _In_ const char* name, - _In_ OrtAllocator* allocator, - _Outptr_ void** buffer, - _Out_ size_t* data_size); - /** \brief Function called by ORT to allow user to specify how an initializer should be saved, that is, either * written to an external file or stored within the model. ORT calls this function for every initializer when * generating a model. @@ -7568,30 +7511,6 @@ struct OrtApi { * \since Version 1.28. */ ORT_API_T(OrtExperimentalFnPtr, GetExperimentalFunction, _In_ const char* name); - - /** \brief Registers a callback to provide EPContext binary data during session load. - * - * When loading a compiled model with external (non-embedded) EPContext binary data, an execution provider can - * retrieve this callback from OrtEpContextConfig and call it instead of reading the binary data from disk. - * - * Reading happens at session load, so this callback is configured on OrtSessionOptions. The corresponding write - * callback runs only at compile time and is configured on OrtModelCompilationOptions via - * OrtCompileApi::ModelCompilationOptions_SetEpContextDataWriteFunc. - * - * The state pointer is stored as-is and is not owned by ORT. It must remain valid while any session or EP created - * from these options may call the callback. If the same state may be used by multiple EPs or threads, the application - * is responsible for synchronization. - * - * \param[in] options The OrtSessionOptions instance. - * \param[in] read_func The OrtReadNamedBufferFunc callback. - * \param[in] state Opaque state passed to read_func. Can be NULL. - * - * \snippet{doc} snippets.dox OrtStatus Return Value - * - * \since Version 1.28. - */ - ORT_API2_STATUS(SessionOptions_SetEpContextDataReadFunc, _Inout_ OrtSessionOptions* options, - _In_ OrtReadNamedBufferFunc read_func, _In_opt_ void* state); }; /* @@ -8434,31 +8353,6 @@ struct OrtCompileApi { ORT_API2_STATUS(ModelCompilationOptions_SetInputModel, _In_ OrtModelCompilationOptions* model_compile_options, _In_ const OrtModel* model); - - /** \brief Sets a callback for writing EPContext binary data during compilation. - * - * When EPContext embed mode is disabled, execution providers can retrieve this callback from OrtEpContextConfig and - * call it instead of writing EPContext binary data directly to disk. - * - * Writing happens only at compile time, so this callback is configured on OrtModelCompilationOptions. The - * corresponding read callback runs at session load and is configured on OrtSessionOptions via - * OrtApi::SessionOptions_SetEpContextDataReadFunc. - * - * The state pointer is stored as-is and is not owned by ORT. It must remain valid for the duration of the compile - * operation that may call the callback. If the same state may be used by multiple EPs or threads, the application is - * responsible for synchronization. - * - * \param[in] model_compile_options The OrtModelCompilationOptions instance. - * \param[in] write_func The OrtWriteNamedBufferFunc callback used to write EPContext bytes. - * \param[in] state Opaque state passed to write_func. Can be NULL. - * - * \snippet{doc} snippets.dox OrtStatus Return Value - * - * \since Version 1.28. - */ - ORT_API2_STATUS(ModelCompilationOptions_SetEpContextDataWriteFunc, - _In_ OrtModelCompilationOptions* model_compile_options, - _In_ OrtWriteNamedBufferFunc write_func, _In_opt_ void* state); }; /** diff --git a/include/onnxruntime/core/session/onnxruntime_cxx_api.h b/include/onnxruntime/core/session/onnxruntime_cxx_api.h index f5bc169ea1422..4798d3d4ad1b8 100644 --- a/include/onnxruntime/core/session/onnxruntime_cxx_api.h +++ b/include/onnxruntime/core/session/onnxruntime_cxx_api.h @@ -658,7 +658,6 @@ ORT_DEFINE_RELEASE(Value); ORT_DEFINE_RELEASE(ValueInfo); ORT_DEFINE_RELEASE_FROM_API_STRUCT(ModelCompilationOptions, GetCompileApi); -ORT_DEFINE_RELEASE_FROM_API_STRUCT(EpContextConfig, GetEpApi); ORT_DEFINE_RELEASE_FROM_API_STRUCT(EpDevice, GetEpApi); ORT_DEFINE_RELEASE_FROM_API_STRUCT(KernelDef, GetEpApi); ORT_DEFINE_RELEASE_FROM_API_STRUCT(KernelDefBuilder, GetEpApi); @@ -787,7 +786,6 @@ struct AllocatedFree { struct AllocatorWithDefaultOptions; struct Env; -struct EpContextConfig; struct EpDevice; struct ExternalInitializerInfo; struct Graph; @@ -1187,21 +1185,6 @@ struct EpDevice : detail::EpDeviceImpl { ConstKeyValuePairs ep_metadata = {}, ConstKeyValuePairs ep_options = {}); }; -/** \brief Owning wrapper around ::OrtEpContextConfig. */ -struct EpContextConfig : detail::Base { - explicit EpContextConfig(std::nullptr_t) {} ///< No instance is created - explicit EpContextConfig(OrtEpContextConfig* p) : Base{p} {} ///< Take ownership - - /// \brief Wraps OrtEpApi::SessionOptions_GetEpContextConfig - explicit EpContextConfig(const OrtSessionOptions* session_options); - - /// \brief Wraps OrtEpApi::EpContextConfig_GetEpContextDataReadFunc - std::pair GetEpContextDataReadFunc() const; - - /// \brief Wraps OrtEpApi::EpContextConfig_GetEpContextDataWriteFunc - std::pair GetEpContextDataWriteFunc() const; -}; - /** \brief Validate a compiled model's compatibility for one or more EP devices. * * Throws on error. Returns the resulting compatibility status. @@ -1685,8 +1668,6 @@ struct SessionOptionsImpl : ConstSessionOptionsImpl { const std::vector& external_initializer_file_buffer_array, const std::vector& external_initializer_file_lengths); ///< Wraps OrtApi::AddExternalInitializersFromFilesInMemory - SessionOptionsImpl& SetEpContextDataReadFunc(OrtReadNamedBufferFunc read_func, void* state); ///< Wraps OrtApi::SessionOptions_SetEpContextDataReadFunc - SessionOptionsImpl& AppendExecutionProvider_CPU(int use_arena); ///< Wraps OrtApi::SessionOptionsAppendExecutionProvider_CPU SessionOptionsImpl& AppendExecutionProvider_CUDA(const OrtCUDAProviderOptions& provider_options); ///< Wraps OrtApi::SessionOptionsAppendExecutionProvider_CUDA SessionOptionsImpl& AppendExecutionProvider_CUDA_V2(const OrtCUDAProviderOptionsV2& provider_options); ///< Wraps OrtApi::SessionOptionsAppendExecutionProvider_CUDA_V2 @@ -1788,9 +1769,6 @@ struct ModelCompilationOptions : detail::Base { ///< Wraps OrtApi::ModelCompilationOptions_SetOutputModelWriteFunc ModelCompilationOptions& SetOutputModelWriteFunc(OrtWriteBufferFunc write_func, void* state); - ///< Wraps OrtCompileApi::ModelCompilationOptions_SetEpContextDataWriteFunc - ModelCompilationOptions& SetEpContextDataWriteFunc(OrtWriteNamedBufferFunc write_func, void* state); - ModelCompilationOptions& SetEpContextBinaryInformation(const ORTCHAR_T* output_directory, const ORTCHAR_T* model_name); ///< Wraps OrtApi::ModelCompilationOptions_SetEpContextBinaryInformation ModelCompilationOptions& SetFlags(uint32_t flags); ///< Wraps OrtApi::ModelCompilationOptions_SetFlags diff --git a/include/onnxruntime/core/session/onnxruntime_cxx_inline.h b/include/onnxruntime/core/session/onnxruntime_cxx_inline.h index fe1478d789715..d7439e7b356c6 100644 --- a/include/onnxruntime/core/session/onnxruntime_cxx_inline.h +++ b/include/onnxruntime/core/session/onnxruntime_cxx_inline.h @@ -769,24 +769,6 @@ inline EpDevice::EpDevice(OrtEpFactory& ep_factory, ConstHardwareDevice& hardwar ThrowOnError(GetEpApi().CreateEpDevice(&ep_factory, hardware_device, ep_metadata, ep_options, &p_)); } -inline EpContextConfig::EpContextConfig(const OrtSessionOptions* session_options) { - ThrowOnError(GetEpApi().SessionOptions_GetEpContextConfig(session_options, &p_)); -} - -inline std::pair EpContextConfig::GetEpContextDataReadFunc() const { - OrtReadNamedBufferFunc read_func = nullptr; - void* state = nullptr; - ThrowOnError(GetEpApi().EpContextConfig_GetEpContextDataReadFunc(this->p_, &read_func, &state)); - return {read_func, state}; -} - -inline std::pair EpContextConfig::GetEpContextDataWriteFunc() const { - OrtWriteNamedBufferFunc write_func = nullptr; - void* state = nullptr; - ThrowOnError(GetEpApi().EpContextConfig_GetEpContextDataWriteFunc(this->p_, &write_func, &state)); - return {write_func, state}; -} - namespace detail { template inline std::string EpAssignedSubgraphImpl::GetEpName() const { @@ -1353,12 +1335,6 @@ inline ModelCompilationOptions& ModelCompilationOptions::SetOutputModelWriteFunc return *this; } -inline ModelCompilationOptions& ModelCompilationOptions::SetEpContextDataWriteFunc( - OrtWriteNamedBufferFunc write_func, void* state) { - Ort::ThrowOnError(GetCompileApi().ModelCompilationOptions_SetEpContextDataWriteFunc(this->p_, write_func, state)); - return *this; -} - inline ModelCompilationOptions& ModelCompilationOptions::SetEpContextEmbedMode( bool embed_ep_context_in_model) { Ort::ThrowOnError(GetCompileApi().ModelCompilationOptions_SetEpContextEmbedMode( @@ -1598,13 +1574,6 @@ inline SessionOptionsImpl& SessionOptionsImpl::AddExternalInitializersFrom return *this; } -template -inline SessionOptionsImpl& SessionOptionsImpl::SetEpContextDataReadFunc(OrtReadNamedBufferFunc read_func, - void* state) { - ThrowOnError(GetApi().SessionOptions_SetEpContextDataReadFunc(this->p_, read_func, state)); - return *this; -} - template inline SessionOptionsImpl& SessionOptionsImpl::AppendExecutionProvider_CPU(int use_arena) { ThrowOnError(OrtSessionOptionsAppendExecutionProvider_CPU(this->p_, use_arena)); diff --git a/include/onnxruntime/core/session/onnxruntime_ep_c_api.h b/include/onnxruntime/core/session/onnxruntime_ep_c_api.h index 8243ce1c511b8..6824b85ba3001 100644 --- a/include/onnxruntime/core/session/onnxruntime_ep_c_api.h +++ b/include/onnxruntime/core/session/onnxruntime_ep_c_api.h @@ -18,7 +18,6 @@ extern "C" { * @{ */ ORT_RUNTIME_CLASS(Ep); -ORT_RUNTIME_CLASS(EpContextConfig); ORT_RUNTIME_CLASS(EpFactory); ORT_RUNTIME_CLASS(EpGraphSupportInfo); ORT_RUNTIME_CLASS(MemoryDevice); // opaque class to wrap onnxruntime::OrtDevice @@ -2078,75 +2077,6 @@ struct OrtEpApi { ORT_API2_STATUS(ProfilingEventsContainer_AddEvents, _In_ OrtProfilingEventsContainer* events_container, _In_reads_(num_events) const OrtProfilingEvent* const* events, _In_ size_t num_events); - - /** \brief Get the EPContext configuration from session options. - * - * Extracts EPContext-related data I/O callbacks from the session options into an opaque OrtEpContextConfig handle. - * The EP should call this during CreateEp() while session_options is still valid, and store the returned handle for - * use during Compile(). The returned config is always non-NULL and must be released with ReleaseEpContextConfig. - * - * The returned handle owns only ORT's copy of callback function pointers and opaque state pointer values. It does not - * own the application-provided state. The application is responsible for keeping callback state valid and - * synchronized while an EP may call callbacks retrieved from this config. - * - * \param[in] session_options The OrtSessionOptions instance. - * \param[out] config The extracted OrtEpContextConfig. - * - * \snippet{doc} snippets.dox OrtStatus Return Value - * - * \since Version 1.28. - */ - ORT_API2_STATUS(SessionOptions_GetEpContextConfig, - _In_ const OrtSessionOptions* session_options, - _Outptr_ OrtEpContextConfig** config); - - /** \brief Release an OrtEpContextConfig instance. - * - * \param[in] input The OrtEpContextConfig instance to release. May be NULL. - * - * \since Version 1.28. - */ - ORT_CLASS_RELEASE(EpContextConfig); - - /** \brief Get the application-provided EPContext data read callback. - * - * Returns the OrtReadNamedBufferFunc and opaque state pointer registered via - * OrtApi::SessionOptions_SetEpContextDataReadFunc. If no callback was registered, *read_func and *state are set to - * NULL. The EP is responsible for calling the callback when present and for using its own normal read path when no - * callback is present. - * - * \param[in] config The OrtEpContextConfig from SessionOptions_GetEpContextConfig. - * \param[out] read_func The registered read callback, or NULL if none was registered. - * \param[out] state Opaque state pointer passed to read_func, or NULL if none was registered. - * - * \snippet{doc} snippets.dox OrtStatus Return Value - * - * \since Version 1.28. - */ - ORT_API2_STATUS(EpContextConfig_GetEpContextDataReadFunc, - _In_ const OrtEpContextConfig* config, - _Out_ OrtReadNamedBufferFunc* read_func, - _Out_ void** state); - - /** \brief Get the application-provided EPContext data write callback. - * - * Returns the OrtWriteNamedBufferFunc and opaque state pointer registered via - * OrtCompileApi::ModelCompilationOptions_SetEpContextDataWriteFunc. If no callback was registered, *write_func and - * *state are set to NULL. The EP is responsible for calling the callback when present and for using its own normal - * write path when no callback is present. - * - * \param[in] config The OrtEpContextConfig from SessionOptions_GetEpContextConfig. - * \param[out] write_func The registered write callback, or NULL if none was registered. - * \param[out] state Opaque state pointer passed to write_func, or NULL if none was registered. - * - * \snippet{doc} snippets.dox OrtStatus Return Value - * - * \since Version 1.28. - */ - ORT_API2_STATUS(EpContextConfig_GetEpContextDataWriteFunc, - _In_ const OrtEpContextConfig* config, - _Out_ OrtWriteNamedBufferFunc* write_func, - _Out_ void** state); }; /** diff --git a/include/onnxruntime/core/session/onnxruntime_experimental_c_api.h b/include/onnxruntime/core/session/onnxruntime_experimental_c_api.h index 0dd87c10776d3..0d06a38c1ff69 100644 --- a/include/onnxruntime/core/session/onnxruntime_experimental_c_api.h +++ b/include/onnxruntime/core/session/onnxruntime_experimental_c_api.h @@ -43,6 +43,68 @@ ORT_RUNTIME_CLASS(ModelPackageOptions); ORT_RUNTIME_CLASS(ModelPackageContext); ORT_RUNTIME_CLASS(ModelPackageComponentContext); +// Opaque handle holding the EPContext callbacks and opaque state extracted from an OrtSessionOptions instance. Used by +// the experimental OrtEpApi_* EPContext data functions. Create via OrtEpApi_SessionOptions_GetEpContextConfig and +// release with OrtEpApi_ReleaseEpContextConfig. +ORT_RUNTIME_CLASS(EpContextConfig); + +/** \brief Function called to write named binary data. + * + * This callback is currently used for EPContext binary data, but its contract is intentionally generic so future APIs + * can reuse it for other named data payloads. The callback is called synchronously by the component that receives it. + * ORT does not own or retain buffer after the callback returns. ORT does not serialize invocations made by different + * EP instances or worker threads. + * + * Each callback invocation represents one complete write operation for name. The callback signature does not + * provide an offset, sequence number, or final-chunk marker, so the component invoking the callback must define any + * chunked ordering and completion contract with the application. Current EPContext use should prefer a single callback + * invocation per EPContext binary unless chunking semantics are documented by the EP. + * + * The application's implementation can process the data in any way (e.g., encrypt and store, upload to cloud storage, + * or compress) before persisting it. + * + * \param[in] state Opaque pointer holding the user's state. ORT does not own or manage this pointer. The application + * must keep it valid for the duration required by the API that accepted the callback and must provide + * any synchronization required if it can be used concurrently. + * \param[in] name The file name or logical data identifier as a null-terminated UTF-8 string. + * \param[in] buffer The buffer containing data to write. + * \param[in] buffer_num_bytes The size of the buffer in bytes. + * + * \return OrtStatus* Write status. Return nullptr on success. + * Use CreateStatus to provide error info with ORT_FAIL as the error code. + * ORT will release the OrtStatus* if not null. + */ +typedef OrtStatus*(ORT_API_CALL* OrtWriteNamedBufferFunc)(_In_ void* state, + _In_ const char* name, + _In_ const void* buffer, + _In_ size_t buffer_num_bytes); + +/** \brief Function called to read named binary data. + * + * This callback is currently used for EPContext binary data, but its contract is intentionally generic so future APIs + * can reuse it for other named data payloads. The application reads, processes (e.g., decrypts, decompresses, + * downloads), and returns the requested data. ORT provides an allocator so the application can allocate the output + * buffer directly. The callback is called synchronously by the component that receives it. ORT does not serialize + * invocations made by different EP instances or worker threads. + * + * \param[in] state Opaque pointer holding the user's state. ORT does not own or manage this pointer. The application + * must keep it valid for the duration required by the API that accepted the callback and must provide + * any synchronization required if it can be used concurrently. + * \param[in] name The file name or logical data identifier to read as a null-terminated UTF-8 string. + * \param[in] allocator ORT-provided allocator. The application must use this to allocate the output buffer. + * \param[out] buffer Set by the implementation to the allocated buffer containing the output data. + * \param[out] data_size Set by the implementation to the size of the output data in bytes. + * + * \return OrtStatus* Read status. Return nullptr on success. + * Use CreateStatus to provide error info with ORT_FAIL as the error code. + * ORT will release the OrtStatus* if not null. + */ +typedef OrtStatus*(ORT_API_CALL* OrtReadNamedBufferFunc)(_In_ void* state, + _In_ const char* name, + _In_ OrtAllocator* allocator, + _Outptr_ void** buffer, + _Out_ size_t* data_size); + // // C: function pointer typedefs and name constants // diff --git a/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc b/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc index 57a4e472b6f6d..a05f64ee810a7 100644 --- a/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc +++ b/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc @@ -282,3 +282,106 @@ ORT_EXPERIMENTAL_API(28, OrtStatusPtr, OrtModelPackageApi_CreateSession, _In_ OrtModelPackageComponentContext* context, _In_opt_ const OrtSessionOptions* session_options, _Outptr_ OrtSession** session) + +/** \brief Registers a callback to provide EPContext binary data during session load. + * + * When loading a compiled model with external (non-embedded) EPContext binary data, an execution provider can + * retrieve this callback from OrtEpContextConfig and call it instead of reading the binary data from disk. + * + * Reading happens at session load, so this callback is configured on OrtSessionOptions. The corresponding write + * callback runs only at compile time and is configured on OrtModelCompilationOptions via + * OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc. + * + * The state pointer is stored as-is and is not owned by ORT. It must remain valid while any session or EP created + * from these options may call the callback. If the same state may be used by multiple EPs or threads, the application + * is responsible for synchronization. + * + * \param[in] options The OrtSessionOptions instance. + * \param[in] read_func The OrtReadNamedBufferFunc callback. + * \param[in] state Opaque state passed to read_func. Can be NULL. + * + * \snippet{doc} snippets.dox OrtStatus Return Value + */ +ORT_EXPERIMENTAL_API(28, OrtStatusPtr, OrtApi_SessionOptions_SetEpContextDataReadFunc, + _Inout_ OrtSessionOptions* options, _In_ OrtReadNamedBufferFunc read_func, _In_opt_ void* state) + +/** \brief Sets a callback for writing EPContext binary data during compilation. + * + * When EPContext embed mode is disabled, execution providers can retrieve this callback from OrtEpContextConfig and + * call it instead of writing EPContext binary data directly to disk. + * + * Writing happens only at compile time, so this callback is configured on OrtModelCompilationOptions. The + * corresponding read callback runs at session load and is configured on OrtSessionOptions via + * OrtApi_SessionOptions_SetEpContextDataReadFunc. + * + * The state pointer is stored as-is and is not owned by ORT. It must remain valid for the duration of the compile + * operation that may call the callback. If the same state may be used by multiple EPs or threads, the application is + * responsible for synchronization. + * + * \param[in] model_compile_options The OrtModelCompilationOptions instance. + * \param[in] write_func The OrtWriteNamedBufferFunc callback used to write EPContext bytes. + * \param[in] state Opaque state passed to write_func. Can be NULL. + * + * \snippet{doc} snippets.dox OrtStatus Return Value + */ +ORT_EXPERIMENTAL_API(28, OrtStatusPtr, OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc, + _In_ OrtModelCompilationOptions* model_compile_options, _In_ OrtWriteNamedBufferFunc write_func, + _In_opt_ void* state) + +/** \brief Extracts the EPContext configuration (callbacks and state) from an OrtSessionOptions instance. + * + * The EP should call this during CreateEp() while session_options is still valid, and store the returned handle for + * use during Compile(). The returned config is always non-NULL and must be released with + * OrtEpApi_ReleaseEpContextConfig. + * + * The returned handle owns only ORT's copy of callback function pointers and opaque state pointer values. It does not + * own the application-provided state. The application is responsible for keeping callback state valid and + * synchronized while an EP may call callbacks retrieved from this config. + * + * \param[in] session_options The OrtSessionOptions instance. + * \param[out] config The extracted OrtEpContextConfig. + * + * \snippet{doc} snippets.dox OrtStatus Return Value + */ +ORT_EXPERIMENTAL_API(28, OrtStatusPtr, OrtEpApi_SessionOptions_GetEpContextConfig, + _In_ const OrtSessionOptions* session_options, _Outptr_ OrtEpContextConfig** config) + +/** \brief Release an OrtEpContextConfig instance. + * + * \param[in] config The OrtEpContextConfig instance to release. May be NULL. + */ +ORT_EXPERIMENTAL_API(28, void, OrtEpApi_ReleaseEpContextConfig, _Frees_ptr_opt_ OrtEpContextConfig* config) + +/** \brief Get the application-provided EPContext data read callback. + * + * Returns the OrtReadNamedBufferFunc and opaque state pointer registered via + * OrtApi_SessionOptions_SetEpContextDataReadFunc. If no callback was registered, *read_func and *state are set to + * NULL. The EP is responsible for calling the callback when present and for using its own normal read path when no + * callback is present. + * + * \param[in] config The OrtEpContextConfig from OrtEpApi_SessionOptions_GetEpContextConfig. + * \param[out] read_func The registered read callback, or NULL if none was registered. + * \param[out] state Opaque state pointer passed to read_func, or NULL if none was registered. + * + * \snippet{doc} snippets.dox OrtStatus Return Value + */ +ORT_EXPERIMENTAL_API(28, OrtStatusPtr, OrtEpApi_EpContextConfig_GetEpContextDataReadFunc, + _In_ const OrtEpContextConfig* config, _Out_ OrtReadNamedBufferFunc* read_func, + _Out_ void** state) + +/** \brief Get the application-provided EPContext data write callback. + * + * Returns the OrtWriteNamedBufferFunc and opaque state pointer registered via + * OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc. If no callback was registered, *write_func and + * *state are set to NULL. The EP is responsible for calling the callback when present and for using its own normal + * write path when no callback is present. + * + * \param[in] config The OrtEpContextConfig from OrtEpApi_SessionOptions_GetEpContextConfig. + * \param[out] write_func The registered write callback, or NULL if none was registered. + * \param[out] state Opaque state pointer passed to write_func, or NULL if none was registered. + * + * \snippet{doc} snippets.dox OrtStatus Return Value + */ +ORT_EXPERIMENTAL_API(28, OrtStatusPtr, OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc, + _In_ const OrtEpContextConfig* config, _Out_ OrtWriteNamedBufferFunc* write_func, + _Out_ void** state) diff --git a/onnxruntime/core/framework/ep_context_options.h b/onnxruntime/core/framework/ep_context_options.h index aa9053b60438e..29fa796272c9f 100644 --- a/onnxruntime/core/framework/ep_context_options.h +++ b/onnxruntime/core/framework/ep_context_options.h @@ -7,6 +7,7 @@ #include #include "core/framework/allocator.h" #include "core/framework/config_options.h" +#include "core/session/onnxruntime_experimental_c_api.h" namespace onnxruntime { namespace epctx { diff --git a/onnxruntime/core/framework/session_options.h b/onnxruntime/core/framework/session_options.h index 5c8ba0e62061a..6f7755207e492 100644 --- a/onnxruntime/core/framework/session_options.h +++ b/onnxruntime/core/framework/session_options.h @@ -16,6 +16,7 @@ #include "core/framework/ep_context_options.h" #include "core/framework/ort_value.h" #include "core/session/onnxruntime_c_api.h" +#include "core/session/onnxruntime_experimental_c_api.h" #include "core/optimizer/graph_transformer_level.h" #include "core/util/thread_utils.h" diff --git a/onnxruntime/core/session/abi_session_options.cc b/onnxruntime/core/session/abi_session_options.cc index f24bc52814a7d..3d2d61d409afa 100644 --- a/onnxruntime/core/session/abi_session_options.cc +++ b/onnxruntime/core/session/abi_session_options.cc @@ -134,18 +134,6 @@ ORT_API_STATUS_IMPL(OrtApis::GetSessionExecutionMode, _In_ const OrtSessionOptio API_IMPL_END } -ORT_API_STATUS_IMPL(OrtApis::SessionOptions_SetEpContextDataReadFunc, _Inout_ OrtSessionOptions* options, - _In_ OrtReadNamedBufferFunc read_func, _In_opt_ void* state) { - API_IMPL_BEGIN - ORT_API_RETURN_IF(options == nullptr, ORT_INVALID_ARGUMENT, "'options' parameter must not be NULL"); - ORT_API_RETURN_IF(read_func == nullptr, ORT_INVALID_ARGUMENT, "'read_func' parameter must not be NULL"); - - options->value.ep_context_data_read_func = read_func; - options->value.ep_context_data_read_state = state; - return nullptr; - API_IMPL_END -} - // set filepath to save optimized onnx model. ORT_API_STATUS_IMPL(OrtApis::SetOptimizedModelFilePath, _In_ OrtSessionOptions* options, _In_ const ORTCHAR_T* optimized_model_filepath) { options->value.optimized_model_filepath = optimized_model_filepath; diff --git a/onnxruntime/core/session/compile_api.cc b/onnxruntime/core/session/compile_api.cc index 819e31d6eb416..54d26021d8c99 100644 --- a/onnxruntime/core/session/compile_api.cc +++ b/onnxruntime/core/session/compile_api.cc @@ -259,32 +259,6 @@ ORT_API_STATUS_IMPL(OrtCompileAPI::ModelCompilationOptions_SetOutputModelGetInit API_IMPL_END } -ORT_API_STATUS_IMPL(OrtCompileAPI::ModelCompilationOptions_SetEpContextDataWriteFunc, - _In_ OrtModelCompilationOptions* ort_model_compile_options, - _In_ OrtWriteNamedBufferFunc write_func, _In_opt_ void* state) { - API_IMPL_BEGIN -#if !defined(ORT_MINIMAL_BUILD) - auto model_compile_options = reinterpret_cast(ort_model_compile_options); - - if (model_compile_options == nullptr) { - return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, "OrtModelCompilationOptions is NULL"); - } - - if (write_func == nullptr) { - return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, "OrtWriteNamedBufferFunc function is NULL"); - } - - model_compile_options->SetEpContextDataWriteFunc(write_func, state); - return nullptr; -#else - ORT_UNUSED_PARAMETER(ort_model_compile_options); - ORT_UNUSED_PARAMETER(write_func); - ORT_UNUSED_PARAMETER(state); - return OrtApis::CreateStatus(ORT_NOT_IMPLEMENTED, "Compile API is not supported in this build"); -#endif // !defined(ORT_MINIMAL_BUILD) - API_IMPL_END -} - ORT_API_STATUS_IMPL(OrtCompileAPI::ModelCompilationOptions_SetEpContextEmbedMode, _In_ OrtModelCompilationOptions* ort_model_compile_options, bool embed_ep_context_in_model) { @@ -393,11 +367,6 @@ static constexpr OrtCompileApi ort_compile_api = { &OrtCompileAPI::ModelCompilationOptions_SetInputModel, // End of Version 24 - DO NOT MODIFY ABOVE - // End of Version 25 - DO NOT MODIFY ABOVE - // End of Version 26 - DO NOT MODIFY ABOVE - - &OrtCompileAPI::ModelCompilationOptions_SetEpContextDataWriteFunc, - // End of Version 27 - DO NOT MODIFY ABOVE }; // checks that we don't violate the rule that the functions must remain in the slots they were originally assigned @@ -407,8 +376,6 @@ static_assert(offsetof(OrtCompileApi, ModelCompilationOptions_SetOutputModelGetI "Size of version 23 of Api cannot change"); static_assert(offsetof(OrtCompileApi, ModelCompilationOptions_SetInputModel) / sizeof(void*) == 14, "Size of version 24 of Api cannot change"); -static_assert(offsetof(OrtCompileApi, ModelCompilationOptions_SetEpContextDataWriteFunc) / sizeof(void*) == 15, - "Size of version 27 of Api cannot change"); ORT_API(const OrtCompileApi*, OrtCompileAPI::GetCompileApi) { return &ort_compile_api; diff --git a/onnxruntime/core/session/compile_api.h b/onnxruntime/core/session/compile_api.h index c733153b9aeb5..e8f171ee24295 100644 --- a/onnxruntime/core/session/compile_api.h +++ b/onnxruntime/core/session/compile_api.h @@ -44,8 +44,5 @@ ORT_API_STATUS_IMPL(ModelCompilationOptions_SetOutputModelGetInitializerLocation ORT_API_STATUS_IMPL(ModelCompilationOptions_SetInputModel, _In_ OrtModelCompilationOptions* model_compile_options, _In_ const OrtModel* model); -ORT_API_STATUS_IMPL(ModelCompilationOptions_SetEpContextDataWriteFunc, - _In_ OrtModelCompilationOptions* model_compile_options, - _In_ OrtWriteNamedBufferFunc write_func, _In_opt_ void* state); } // namespace OrtCompileAPI diff --git a/onnxruntime/core/session/experimental_c_api.cc b/onnxruntime/core/session/experimental_c_api.cc index 458c47bcb58cd..b2c31f2bc6b4e 100644 --- a/onnxruntime/core/session/experimental_c_api.cc +++ b/onnxruntime/core/session/experimental_c_api.cc @@ -6,12 +6,29 @@ #include #include +#include +#include "core/common/common.h" #include "core/framework/error_code_helper.h" +#include "core/framework/ep_context_options.h" +#include "core/session/abi_session_options_impl.h" #include "core/session/onnxruntime_c_api.h" #include "core/session/onnxruntime_experimental_c_api.h" #include "core/session/ort_apis.h" +#if !defined(ORT_MINIMAL_BUILD) +#include "core/session/model_compilation_options.h" +#endif // !defined(ORT_MINIMAL_BUILD) + +// Opaque handle backing the experimental OrtEpApi_* EPContext data functions. Holds copies of the application's +// EPContext read/write callbacks and opaque state extracted from an OrtSessionOptions instance. +struct OrtEpContextConfig { + OrtWriteNamedBufferFunc write_func = nullptr; + void* write_state = nullptr; + OrtReadNamedBufferFunc read_func = nullptr; + void* read_state = nullptr; +}; + // --------------------------------------------------------------------------- // Experimental function implementations // --------------------------------------------------------------------------- @@ -40,6 +57,98 @@ ORT_API_STATUS_IMPL(OrtApi_ExperimentalApiTest_SinceV28, API_IMPL_END } +ORT_API_STATUS_IMPL(OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28, _Inout_ OrtSessionOptions* options, + _In_ OrtReadNamedBufferFunc read_func, _In_opt_ void* state) { + API_IMPL_BEGIN + ORT_API_RETURN_IF(options == nullptr, ORT_INVALID_ARGUMENT, "'options' parameter must not be NULL"); + ORT_API_RETURN_IF(read_func == nullptr, ORT_INVALID_ARGUMENT, "'read_func' parameter must not be NULL"); + + options->value.ep_context_data_read_func = read_func; + options->value.ep_context_data_read_state = state; + return nullptr; + API_IMPL_END +} + +ORT_API_STATUS_IMPL(OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28, + _In_ OrtModelCompilationOptions* ort_model_compile_options, + _In_ OrtWriteNamedBufferFunc write_func, _In_opt_ void* state) { + API_IMPL_BEGIN +#if !defined(ORT_MINIMAL_BUILD) + auto model_compile_options = reinterpret_cast(ort_model_compile_options); + + if (model_compile_options == nullptr) { + return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, "OrtModelCompilationOptions is NULL"); + } + + if (write_func == nullptr) { + return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, "OrtWriteNamedBufferFunc function is NULL"); + } + + model_compile_options->SetEpContextDataWriteFunc(write_func, state); + return nullptr; +#else + ORT_UNUSED_PARAMETER(ort_model_compile_options); + ORT_UNUSED_PARAMETER(write_func); + ORT_UNUSED_PARAMETER(state); + return OrtApis::CreateStatus(ORT_NOT_IMPLEMENTED, "Compile API is not supported in this build"); +#endif // !defined(ORT_MINIMAL_BUILD) + API_IMPL_END +} + +ORT_API_STATUS_IMPL(OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28, + _In_ const OrtSessionOptions* session_options, + _Outptr_ OrtEpContextConfig** config) { + API_IMPL_BEGIN + ORT_API_RETURN_IF(session_options == nullptr, ORT_INVALID_ARGUMENT, "OrtSessionOptions is NULL"); + ORT_API_RETURN_IF(config == nullptr, ORT_INVALID_ARGUMENT, "Output OrtEpContextConfig is NULL"); + + auto ep_context_config = std::make_unique(); + if (const auto* write_config = session_options->value.ep_context_gen_options.TryGetEpContextDataWriteFunc()) { + ep_context_config->write_func = write_config->write_func; + ep_context_config->write_state = write_config->state; + } + ep_context_config->read_func = session_options->value.ep_context_data_read_func; + ep_context_config->read_state = session_options->value.ep_context_data_read_state; + + *config = ep_context_config.release(); + return nullptr; + API_IMPL_END +} + +ORT_API(void, OrtEpApi_ReleaseEpContextConfig_SinceV28, _Frees_ptr_opt_ OrtEpContextConfig* config) { + delete config; +} + +ORT_API_STATUS_IMPL(OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28, + _In_ const OrtEpContextConfig* config, + _Out_ OrtReadNamedBufferFunc* read_func, + _Out_ void** state) { + API_IMPL_BEGIN + ORT_API_RETURN_IF(config == nullptr, ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); + ORT_API_RETURN_IF(read_func == nullptr, ORT_INVALID_ARGUMENT, "Output read_func is NULL"); + ORT_API_RETURN_IF(state == nullptr, ORT_INVALID_ARGUMENT, "Output state is NULL"); + + *read_func = config->read_func; + *state = config->read_func != nullptr ? config->read_state : nullptr; + return nullptr; + API_IMPL_END +} + +ORT_API_STATUS_IMPL(OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28, + _In_ const OrtEpContextConfig* config, + _Out_ OrtWriteNamedBufferFunc* write_func, + _Out_ void** state) { + API_IMPL_BEGIN + ORT_API_RETURN_IF(config == nullptr, ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); + ORT_API_RETURN_IF(write_func == nullptr, ORT_INVALID_ARGUMENT, "Output write_func is NULL"); + ORT_API_RETURN_IF(state == nullptr, ORT_INVALID_ARGUMENT, "Output state is NULL"); + + *write_func = config->write_func; + *state = config->write_func != nullptr ? config->write_state : nullptr; + return nullptr; + API_IMPL_END +} + } // namespace OrtExperimentalApis // --------------------------------------------------------------------------- diff --git a/onnxruntime/core/session/model_compilation_options.h b/onnxruntime/core/session/model_compilation_options.h index c92f2362e2413..a15af565c4d54 100644 --- a/onnxruntime/core/session/model_compilation_options.h +++ b/onnxruntime/core/session/model_compilation_options.h @@ -13,6 +13,7 @@ #include "core/graph/model_editor_api_types.h" #include "core/session/abi_session_options_impl.h" #include "core/session/onnxruntime_c_api.h" +#include "core/session/onnxruntime_experimental_c_api.h" #include "core/session/onnxruntime_session_options_config_keys.h" namespace onnxruntime { diff --git a/onnxruntime/core/session/onnxruntime_c_api.cc b/onnxruntime/core/session/onnxruntime_c_api.cc index 60cdcbf760929..f451eaa401497 100644 --- a/onnxruntime/core/session/onnxruntime_c_api.cc +++ b/onnxruntime/core/session/onnxruntime_c_api.cc @@ -4909,7 +4909,6 @@ static constexpr OrtApi ort_api_1_to_28 = { // End of Version 27 - DO NOT MODIFY ABOVE (see above text for more information) &OrtApis::GetExperimentalFunction, - &OrtApis::SessionOptions_SetEpContextDataReadFunc, }; // OrtApiBase can never change as there is no way to know what version of OrtApiBase is returned by OrtGetApiBase. diff --git a/onnxruntime/core/session/ort_apis.h b/onnxruntime/core/session/ort_apis.h index 60cb8ed6674fb..61ece2dd9a682 100644 --- a/onnxruntime/core/session/ort_apis.h +++ b/onnxruntime/core/session/ort_apis.h @@ -66,8 +66,6 @@ ORT_API_STATUS_IMPL(EnableMemPattern, _In_ OrtSessionOptions* options); ORT_API_STATUS_IMPL(DisableMemPattern, _In_ OrtSessionOptions* options); ORT_API_STATUS_IMPL(GetMemPatternEnabled, _In_ const OrtSessionOptions* options, _Out_ int* out); ORT_API_STATUS_IMPL(GetSessionExecutionMode, _In_ const OrtSessionOptions* options, _Out_ ExecutionMode* out); -ORT_API_STATUS_IMPL(SessionOptions_SetEpContextDataReadFunc, _Inout_ OrtSessionOptions* options, - _In_ OrtReadNamedBufferFunc read_func, _In_opt_ void* state); ORT_API_STATUS_IMPL(EnableCpuMemArena, _In_ OrtSessionOptions* options); ORT_API_STATUS_IMPL(DisableCpuMemArena, _In_ OrtSessionOptions* options); ORT_API_STATUS_IMPL(SetSessionLogId, _In_ OrtSessionOptions* options, const char* logid); diff --git a/onnxruntime/core/session/plugin_ep/ep_api.cc b/onnxruntime/core/session/plugin_ep/ep_api.cc index 846c786c7f283..d56f4299402b5 100644 --- a/onnxruntime/core/session/plugin_ep/ep_api.cc +++ b/onnxruntime/core/session/plugin_ep/ep_api.cc @@ -24,7 +24,6 @@ #include "core/graph/onnx_protobuf.h" #include "core/session/abi_devices.h" #include "core/session/abi_ep_types.h" -#include "core/session/abi_session_options_impl.h" #include "core/session/abi_opschema.h" #include "core/session/environment.h" #include "core/session/onnxruntime_ep_device_ep_metadata_keys.h" @@ -37,16 +36,7 @@ #include "core/session/plugin_ep/ep_event_profiling.h" using namespace onnxruntime; - -struct OrtEpContextConfig { - OrtWriteNamedBufferFunc write_func = nullptr; - void* write_state = nullptr; - OrtReadNamedBufferFunc read_func = nullptr; - void* read_state = nullptr; -}; - namespace OrtExecutionProviderApi { - ORT_API_STATUS_IMPL(CreateEpDevice, _In_ OrtEpFactory* ep_factory, _In_ const OrtHardwareDevice* hardware_device, _In_opt_ const OrtKeyValuePairs* ep_metadata, @@ -1208,60 +1198,6 @@ ORT_API_STATUS_IMPL(ProfilingEventsContainer_AddEvents, API_IMPL_END } -ORT_API_STATUS_IMPL(SessionOptions_GetEpContextConfig, - _In_ const OrtSessionOptions* session_options, - _Outptr_ OrtEpContextConfig** config) { - API_IMPL_BEGIN - ORT_API_RETURN_IF(session_options == nullptr, ORT_INVALID_ARGUMENT, "OrtSessionOptions is NULL"); - ORT_API_RETURN_IF(config == nullptr, ORT_INVALID_ARGUMENT, "Output OrtEpContextConfig is NULL"); - - auto ep_context_config = std::make_unique(); - if (const auto* write_config = session_options->value.ep_context_gen_options.TryGetEpContextDataWriteFunc()) { - ep_context_config->write_func = write_config->write_func; - ep_context_config->write_state = write_config->state; - } - ep_context_config->read_func = session_options->value.ep_context_data_read_func; - ep_context_config->read_state = session_options->value.ep_context_data_read_state; - - *config = ep_context_config.release(); - return nullptr; - API_IMPL_END -} - -ORT_API(void, ReleaseEpContextConfig, _Frees_ptr_opt_ OrtEpContextConfig* config) { - delete config; -} - -ORT_API_STATUS_IMPL(EpContextConfig_GetEpContextDataReadFunc, - _In_ const OrtEpContextConfig* config, - _Out_ OrtReadNamedBufferFunc* read_func, - _Out_ void** state) { - API_IMPL_BEGIN - ORT_API_RETURN_IF(config == nullptr, ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); - ORT_API_RETURN_IF(read_func == nullptr, ORT_INVALID_ARGUMENT, "Output read_func is NULL"); - ORT_API_RETURN_IF(state == nullptr, ORT_INVALID_ARGUMENT, "Output state is NULL"); - - *read_func = config->read_func; - *state = config->read_func != nullptr ? config->read_state : nullptr; - return nullptr; - API_IMPL_END -} - -ORT_API_STATUS_IMPL(EpContextConfig_GetEpContextDataWriteFunc, - _In_ const OrtEpContextConfig* config, - _Out_ OrtWriteNamedBufferFunc* write_func, - _Out_ void** state) { - API_IMPL_BEGIN - ORT_API_RETURN_IF(config == nullptr, ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); - ORT_API_RETURN_IF(write_func == nullptr, ORT_INVALID_ARGUMENT, "Output write_func is NULL"); - ORT_API_RETURN_IF(state == nullptr, ORT_INVALID_ARGUMENT, "Output state is NULL"); - - *write_func = config->write_func; - *state = config->write_func != nullptr ? config->write_state : nullptr; - return nullptr; - API_IMPL_END -} - static constexpr OrtEpApi ort_ep_api = { // NOTE: ABI compatibility depends on the order within this struct so all additions must be at the end, // and no functions can be removed (the implementation needs to change to return an error). @@ -1351,13 +1287,6 @@ static constexpr OrtEpApi ort_ep_api = { &OrtExecutionProviderApi::ProfilingEvent_GetArgValue, &OrtExecutionProviderApi::ProfilingEventsContainer_AddEvents, // End of Version 25 - DO NOT MODIFY ABOVE - // End of Version 26 - DO NOT MODIFY ABOVE - - &OrtExecutionProviderApi::SessionOptions_GetEpContextConfig, - &OrtExecutionProviderApi::ReleaseEpContextConfig, - &OrtExecutionProviderApi::EpContextConfig_GetEpContextDataReadFunc, - &OrtExecutionProviderApi::EpContextConfig_GetEpContextDataWriteFunc, - // End of Version 27 - DO NOT MODIFY ABOVE }; // checks that we don't violate the rule that the functions must remain in the slots they were originally assigned @@ -1369,8 +1298,6 @@ static_assert(offsetof(OrtEpApi, GetEnvConfigEntries) / sizeof(void*) == 49, "Size of version 24 API cannot change"); static_assert(offsetof(OrtEpApi, ProfilingEventsContainer_AddEvents) / sizeof(void*) == 72, "Size of version 25 API cannot change"); -static_assert(offsetof(OrtEpApi, EpContextConfig_GetEpContextDataWriteFunc) / sizeof(void*) == 76, - "Size of version 27 API cannot change"); } // namespace OrtExecutionProviderApi diff --git a/onnxruntime/core/session/plugin_ep/ep_api.h b/onnxruntime/core/session/plugin_ep/ep_api.h index 63029f61a0948..e32e267a75ba5 100644 --- a/onnxruntime/core/session/plugin_ep/ep_api.h +++ b/onnxruntime/core/session/plugin_ep/ep_api.h @@ -179,18 +179,4 @@ ORT_API_STATUS_IMPL(ProfilingEvent_GetDurationUs, _In_ const OrtProfilingEvent* ORT_API_STATUS_IMPL(ProfilingEvent_GetArgValue, _In_ const OrtProfilingEvent* event, _In_ const char* key, _Outptr_result_maybenull_ const char** out); -// EPContext data I/O -ORT_API_STATUS_IMPL(SessionOptions_GetEpContextConfig, - _In_ const OrtSessionOptions* session_options, - _Outptr_ OrtEpContextConfig** config); -ORT_API(void, ReleaseEpContextConfig, _Frees_ptr_opt_ OrtEpContextConfig* config); -ORT_API_STATUS_IMPL(EpContextConfig_GetEpContextDataReadFunc, - _In_ const OrtEpContextConfig* config, - _Out_ OrtReadNamedBufferFunc* read_func, - _Out_ void** state); -ORT_API_STATUS_IMPL(EpContextConfig_GetEpContextDataWriteFunc, - _In_ const OrtEpContextConfig* config, - _Out_ OrtWriteNamedBufferFunc* write_func, - _Out_ void** state); - } // namespace OrtExecutionProviderApi diff --git a/onnxruntime/test/autoep/library/ep_context_data_utils.h b/onnxruntime/test/autoep/library/ep_context_data_utils.h index ce31476ee3693..007adf2f2696b 100644 --- a/onnxruntime/test/autoep/library/ep_context_data_utils.h +++ b/onnxruntime/test/autoep/library/ep_context_data_utils.h @@ -17,6 +17,7 @@ #endif #include "plugin_ep_utils.h" +#include "onnxruntime_experimental_c_api.h" // Sample-only EPContext data helpers. These are intentionally outside the ORT C and EP ABI. namespace ep_context_data_utils { @@ -165,18 +166,20 @@ inline OrtStatus* WriteEpContextDataToFile(const OrtApi& api, const char* file_n return nullptr; } -inline OrtStatus* ReadEpContextDataWithFileFallback(const OrtApi& api, const OrtEpApi& ep_api, - const OrtEpContextConfig* ep_context_config, - const char* file_name, const OrtGraph* graph, - std::vector& data) { +inline OrtStatus* ReadEpContextDataWithFileFallback( + const OrtApi& api, + OrtExperimental_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn get_read_func, + const OrtEpContextConfig* ep_context_config, + const char* file_name, const OrtGraph* graph, + std::vector& data) { if (file_name == nullptr || file_name[0] == '\0') { return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); } OrtReadNamedBufferFunc read_func = nullptr; void* read_state = nullptr; - if (ep_context_config != nullptr) { - RETURN_IF_ERROR(ep_api.EpContextConfig_GetEpContextDataReadFunc(ep_context_config, &read_func, &read_state)); + if (get_read_func != nullptr && ep_context_config != nullptr) { + RETURN_IF_ERROR(get_read_func(ep_context_config, &read_func, &read_state)); } if (read_func == nullptr) { @@ -212,11 +215,13 @@ inline OrtStatus* ReadEpContextDataWithFileFallback(const OrtApi& api, const Ort return nullptr; } -inline OrtStatus* WriteEpContextDataWithFileFallback(const OrtApi& api, const OrtEpApi& ep_api, - const OrtEpContextConfig* ep_context_config, - const char* file_name, const char* fallback_file_name, - const OrtGraph* graph, - const void* buffer, size_t buffer_size) { +inline OrtStatus* WriteEpContextDataWithFileFallback( + const OrtApi& api, + OrtExperimental_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn get_write_func, + const OrtEpContextConfig* ep_context_config, + const char* file_name, const char* fallback_file_name, + const OrtGraph* graph, + const void* buffer, size_t buffer_size) { if (file_name == nullptr || file_name[0] == '\0') { return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); } @@ -227,8 +232,8 @@ inline OrtStatus* WriteEpContextDataWithFileFallback(const OrtApi& api, const Or OrtWriteNamedBufferFunc write_func = nullptr; void* write_state = nullptr; - if (ep_context_config != nullptr) { - RETURN_IF_ERROR(ep_api.EpContextConfig_GetEpContextDataWriteFunc(ep_context_config, &write_func, &write_state)); + if (get_write_func != nullptr && ep_context_config != nullptr) { + RETURN_IF_ERROR(get_write_func(ep_context_config, &write_func, &write_state)); } if (write_func != nullptr) { @@ -242,11 +247,13 @@ inline OrtStatus* WriteEpContextDataWithFileFallback(const OrtApi& api, const Or return WriteEpContextDataToFile(api, fallback_file_name, graph, buffer, buffer_size); } -inline OrtStatus* WriteEpContextDataWithFileFallback(const OrtApi& api, const OrtEpApi& ep_api, - const OrtEpContextConfig* ep_context_config, - const char* file_name, const OrtGraph* graph, - const void* buffer, size_t buffer_size) { - return WriteEpContextDataWithFileFallback(api, ep_api, ep_context_config, file_name, file_name, graph, buffer, +inline OrtStatus* WriteEpContextDataWithFileFallback( + const OrtApi& api, + OrtExperimental_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn get_write_func, + const OrtEpContextConfig* ep_context_config, + const char* file_name, const OrtGraph* graph, + const void* buffer, size_t buffer_size) { + return WriteEpContextDataWithFileFallback(api, get_write_func, ep_context_config, file_name, file_name, graph, buffer, buffer_size); } diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc index 2458f311b1f48..8c5cff4ae1c0b 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc @@ -169,14 +169,14 @@ struct EpContextNodeComputeInfo : NodeComputeInfoBase { }; ExampleEp::ExampleEp(ExampleEpFactory& factory, const std::string& name, const Config& config, const OrtLogger& logger, - Ort::EpContextConfig ep_context_config) + OrtEpContextConfig* ep_context_config) : OrtEp{}, // explicitly call the struct ctor to ensure all optional values are default initialized ApiPtrs{static_cast(factory)}, factory_{factory}, name_{name}, config_{config}, logger_{logger}, - ep_context_config_{std::move(ep_context_config)} { + ep_context_config_{ep_context_config} { ort_version_supported = ORT_API_VERSION; // set to the ORT version we were compiled with. // Initialize the execution provider's function table @@ -196,7 +196,14 @@ ExampleEp::ExampleEp(ExampleEpFactory& factory, const std::string& name, const C ORT_FILE, __LINE__, __FUNCTION__)); } -ExampleEp::~ExampleEp() = default; +ExampleEp::~ExampleEp() { + if (ep_context_config_ != nullptr) { + if (auto* release_ep_context_config = + Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api)) { + release_ep_context_config(ep_context_config_); + } + } +} /*static*/ const char* ORT_API_CALL ExampleEp ::GetNameImpl(const OrtEp* this_ptr) noexcept { @@ -425,7 +432,9 @@ OrtStatus* ORT_API_CALL ExampleEp::CompileImpl(_In_ OrtEp* this_ptr, _In_ const std::vector ep_context_data; RETURN_IF_ERROR(ep_context_data_utils::ReadEpContextDataWithFileFallback( - ep->ort_api, ep->ep_api, ep->ep_context_config_, ep_cache_context.c_str(), ort_graphs[0], + ep->ort_api, + Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&ep->ort_api), + ep->ep_context_config_, ep_cache_context.c_str(), ort_graphs[0], ep_context_data)); (void)ep_context_data; } @@ -554,7 +563,9 @@ OrtStatus* ExampleEp::CreateEpContextNodes(const OrtGraph* graph, fallback_graph = nullptr; } RETURN_IF_ERROR(ep_context_data_utils::WriteEpContextDataWithFileFallback( - ort_api, ep_api, ep_context_config_, ep_ctx.c_str(), fallback_ep_ctx.c_str(), fallback_graph, + ort_api, + Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn(&ort_api), + ep_context_config_, ep_ctx.c_str(), fallback_ep_ctx.c_str(), fallback_graph, ep_context_data.data(), ep_context_data.size())); } attributes[0] = Ort::OpAttr("ep_cache_context", ep_ctx.data(), static_cast(ep_ctx.size()), diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep.h b/onnxruntime/test/autoep/library/example_plugin_ep/ep.h index 4f57bc04ce77e..fe0cf8712af40 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep.h +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep.h @@ -6,6 +6,7 @@ #include #include "../plugin_ep_utils.h" +#include "onnxruntime_experimental_c_api.h" class ExampleEpFactory; @@ -68,7 +69,7 @@ class ExampleEp : public OrtEp, public ApiPtrs { }; ExampleEp(ExampleEpFactory& factory, const std::string& name, const Config& config, const OrtLogger& logger, - Ort::EpContextConfig ep_context_config); + OrtEpContextConfig* ep_context_config); ~ExampleEp(); @@ -126,7 +127,7 @@ class ExampleEp : public OrtEp, public ApiPtrs { std::string name_; Config config_{}; const OrtLogger& logger_; - Ort::EpContextConfig ep_context_config_{nullptr}; + OrtEpContextConfig* ep_context_config_ = nullptr; std::unordered_map> mul_kernels_; std::unordered_map> ep_context_kernels_; std::unordered_map float_initializers_; diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc index 3f7232db8f676..b1890b6996763 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc @@ -237,11 +237,13 @@ OrtStatus* ORT_API_CALL ExampleEpFactory::CreateEpImpl(OrtEpFactory* this_ptr, config.ep_context_output_model_path = std::move(ep_context_output_model_path); config.enable_weightless_ep_context_nodes = weightless_ep_context_nodes_enable == "1"; - OrtEpContextConfig* ep_context_config_raw = nullptr; - RETURN_IF_ERROR(factory->ep_api.SessionOptions_GetEpContextConfig(session_options, &ep_context_config_raw)); - Ort::EpContextConfig ep_context_config{ep_context_config_raw}; + OrtEpContextConfig* ep_context_config = nullptr; + if (auto* get_ep_context_config = + Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&factory->ort_api)) { + RETURN_IF_ERROR(get_ep_context_config(session_options, &ep_context_config)); + } auto dummy_ep = std::make_unique(*factory, factory->ep_name_, config, *logger, - std::move(ep_context_config)); + ep_context_config); *ep = dummy_ep.release(); return nullptr; diff --git a/onnxruntime/test/autoep/test_execution.cc b/onnxruntime/test/autoep/test_execution.cc index e56a027bb214a..3be1f8fd52458 100644 --- a/onnxruntime/test/autoep/test_execution.cc +++ b/onnxruntime/test/autoep/test_execution.cc @@ -14,6 +14,7 @@ #include "core/graph/constants.h" #include "core/graph/onnx_protobuf.h" #include "core/session/onnxruntime_cxx_api.h" +#include "core/session/onnxruntime_experimental_c_api.h" #include "core/session/onnxruntime_session_options_config_keys.h" #include "core/session/onnxruntime_ep_device_ep_metadata_keys.h" #include "nlohmann/json.hpp" @@ -126,11 +127,21 @@ OrtStatus* ORT_API_CALL FakeEpContextConfigGetWriteFunc(const OrtEpContextConfig return nullptr; } -OrtEpApi MakeFakeEpApi() { - OrtEpApi ep_api{}; - ep_api.EpContextConfig_GetEpContextDataReadFunc = FakeEpContextConfigGetReadFunc; - ep_api.EpContextConfig_GetEpContextDataWriteFunc = FakeEpContextConfigGetWriteFunc; - return ep_api; +// Invokes the experimental EPContext read setter on the public C API. +void SetEpContextDataReadFunc(Ort::SessionOptions& session_options, OrtReadNamedBufferFunc read_func, void* state) { + auto set_read_func = + Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_Fn(&Ort::GetApi()); + ASSERT_NE(set_read_func, nullptr); + ASSERT_ORTSTATUS_OK(set_read_func(session_options, read_func, state)); +} + +// Invokes the experimental EPContext write setter on the public C API. +void SetEpContextDataWriteFunc(Ort::ModelCompilationOptions& compile_options, OrtWriteNamedBufferFunc write_func, + void* state) { + auto set_write_func = + Ort::Experimental::Get_OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28_Fn(&Ort::GetApi()); + ASSERT_NE(set_write_func, nullptr); + ASSERT_ORTSTATUS_OK(set_write_func(compile_options, write_func, state)); } void LoadModelProtoFromFile(const ORTCHAR_T* model_file, ONNX_NAMESPACE::ModelProto& model_proto) { @@ -644,7 +655,6 @@ TEST(OrtEpLibrary, EpContextDataUtils_PathHelpersRoundTrip) { TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathAndInvalidArguments) { const auto& api = Ort::GetApi(); - const auto fake_ep_api = MakeFakeEpApi(); std::filesystem::path data_path; data_path = "stale.ctx"; @@ -662,25 +672,26 @@ TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathAndInvalidArguments) { ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataToFile(api, "unused.ctx", nullptr, nullptr, 1), ORT_INVALID_ARGUMENT, "EPContext data buffer must not be null for non-empty data"); - ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback(api, fake_ep_api, nullptr, - "unused.ctx", nullptr, nullptr, 1), + ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback(api, FakeEpContextConfigGetWriteFunc, + nullptr, "unused.ctx", nullptr, + nullptr, 1), ORT_INVALID_ARGUMENT, "EPContext data buffer must not be null for non-empty data"); std::vector data; - ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataWithFileFallback(api, fake_ep_api, nullptr, "", nullptr, - data), + ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataWithFileFallback(api, FakeEpContextConfigGetReadFunc, + nullptr, "", nullptr, data), ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); - ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback(api, fake_ep_api, nullptr, "", nullptr, - nullptr, 0), + ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback(api, FakeEpContextConfigGetWriteFunc, + nullptr, "", nullptr, nullptr, 0), ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback( - api, fake_ep_api, nullptr, "logical_context_data.bin", "", nullptr, nullptr, 0), + api, FakeEpContextConfigGetWriteFunc, nullptr, "logical_context_data.bin", "", nullptr, + nullptr, 0), ORT_INVALID_ARGUMENT, "EPContext data fallback file name must not be empty"); } TEST(OrtEpLibrary, EpContextDataUtils_FileFallbackReadsAndWrites) { const auto& api = Ort::GetApi(); - const auto fake_ep_api = MakeFakeEpApi(); const std::filesystem::path test_dir = PrepareTempTestDir("ort_ep_context_data_utils_file_fallback_test"); auto cleanup = gsl::finally([&]() { std::filesystem::remove_all(test_dir); }); @@ -698,18 +709,19 @@ TEST(OrtEpLibrary, EpContextDataUtils_FileFallbackReadsAndWrites) { const std::filesystem::path wrapper_data_path = test_dir / "wrapper_context_data.bin"; const std::string wrapper_data_file_name = ep_context_data_utils::PathToUtf8String(wrapper_data_path); ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( - api, fake_ep_api, nullptr, wrapper_data_file_name.c_str(), nullptr, payload.data(), payload.size())); + api, FakeEpContextConfigGetWriteFunc, nullptr, wrapper_data_file_name.c_str(), nullptr, payload.data(), + payload.size())); data.clear(); ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataWithFileFallback( - api, fake_ep_api, nullptr, wrapper_data_file_name.c_str(), nullptr, data)); + api, FakeEpContextConfigGetReadFunc, nullptr, wrapper_data_file_name.c_str(), nullptr, data)); EXPECT_EQ(std::string(data.begin(), data.end()), payload); const std::filesystem::path fallback_data_path = test_dir / "fallback_context_data.bin"; const std::string fallback_data_file_name = ep_context_data_utils::PathToUtf8String(fallback_data_path); ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( - api, fake_ep_api, nullptr, "logical_context_data.bin", fallback_data_file_name.c_str(), nullptr, - payload.data(), payload.size())); + api, FakeEpContextConfigGetWriteFunc, nullptr, "logical_context_data.bin", fallback_data_file_name.c_str(), + nullptr, payload.data(), payload.size())); data.clear(); ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataFromFile(api, fallback_data_file_name.c_str(), nullptr, @@ -719,11 +731,11 @@ TEST(OrtEpLibrary, EpContextDataUtils_FileFallbackReadsAndWrites) { const std::filesystem::path empty_data_path = test_dir / "empty_context_data.bin"; const std::string empty_data_file_name = ep_context_data_utils::PathToUtf8String(empty_data_path); ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( - api, fake_ep_api, nullptr, empty_data_file_name.c_str(), nullptr, nullptr, 0)); + api, FakeEpContextConfigGetWriteFunc, nullptr, empty_data_file_name.c_str(), nullptr, nullptr, 0)); data.assign({'s', 't', 'a', 'l', 'e'}); ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataWithFileFallback( - api, fake_ep_api, nullptr, empty_data_file_name.c_str(), nullptr, data)); + api, FakeEpContextConfigGetReadFunc, nullptr, empty_data_file_name.c_str(), nullptr, data)); EXPECT_TRUE(data.empty()); const std::filesystem::path missing_data_path = test_dir / "missing_context_data.bin"; @@ -735,7 +747,6 @@ TEST(OrtEpLibrary, EpContextDataUtils_FileFallbackReadsAndWrites) { TEST(OrtEpLibrary, EpContextDataUtils_CallbackFallbackUsesCallbacks) { const auto& api = Ort::GetApi(); - const auto fake_ep_api = MakeFakeEpApi(); EpContextDataCallbackState read_callback_state; read_callback_state.payload = {'c', 'a', 'l', 'l', 'b', 'a', 'c', 'k'}; @@ -745,14 +756,14 @@ TEST(OrtEpLibrary, EpContextDataUtils_CallbackFallbackUsesCallbacks) { std::vector data; ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataWithFileFallback( - api, fake_ep_api, FakeEpContextConfig(callbacks), "callback_context.bin", nullptr, data)); + api, FakeEpContextConfigGetReadFunc, FakeEpContextConfig(callbacks), "callback_context.bin", nullptr, data)); ASSERT_TRUE(read_callback_state.read_called); EXPECT_EQ(read_callback_state.read_file_name, "callback_context.bin"); EXPECT_EQ(data, read_callback_state.payload); const std::string payload = "callback write payload"; ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( - api, fake_ep_api, FakeEpContextConfig(callbacks), "callback_write_context.bin", nullptr, + api, FakeEpContextConfigGetWriteFunc, FakeEpContextConfig(callbacks), "callback_write_context.bin", nullptr, payload.data(), payload.size())); ASSERT_TRUE(write_callback_state.write_called); EXPECT_EQ(write_callback_state.write_file_name, "callback_write_context.bin"); @@ -761,7 +772,8 @@ TEST(OrtEpLibrary, EpContextDataUtils_CallbackFallbackUsesCallbacks) { write_callback_state = {}; const std::string payload_with_unused_fallback = "callback write payload with unused fallback"; ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( - api, fake_ep_api, FakeEpContextConfig(callbacks), "callback_write_context_unused_fallback.bin", "", nullptr, + api, FakeEpContextConfigGetWriteFunc, FakeEpContextConfig(callbacks), + "callback_write_context_unused_fallback.bin", "", nullptr, payload_with_unused_fallback.data(), payload_with_unused_fallback.size())); ASSERT_TRUE(write_callback_state.write_called); EXPECT_EQ(write_callback_state.write_file_name, "callback_write_context_unused_fallback.bin"); @@ -771,15 +783,14 @@ TEST(OrtEpLibrary, EpContextDataUtils_CallbackFallbackUsesCallbacks) { TEST(OrtEpLibrary, EpContextDataUtils_ReadCallbackRejectsNullBufferForNonEmptyPayload) { const auto& api = Ort::GetApi(); - const auto fake_ep_api = MakeFakeEpApi(); EpContextDataCallbackState read_callback_state; FakeEpContextConfigCallbacks callbacks{LoadInvalidEpContextDataCallback, &read_callback_state, nullptr, nullptr}; std::vector data; ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataWithFileFallback( - api, fake_ep_api, FakeEpContextConfig(callbacks), "invalid_callback_context.bin", nullptr, - data), + api, FakeEpContextConfigGetReadFunc, FakeEpContextConfig(callbacks), + "invalid_callback_context.bin", nullptr, data), ORT_FAIL, "OrtReadNamedBufferFunc returned a null buffer for non-empty EPContext data"); ASSERT_TRUE(read_callback_state.read_called); EXPECT_EQ(read_callback_state.read_file_name, "invalid_callback_context.bin"); @@ -830,7 +841,7 @@ TEST(OrtEpLibrary, PluginEp_GenEpContextModel_EmbedModeDoesNotUseCallbacks) { EpContextDataCallbackState compile_read_callback_state; { Ort::SessionOptions session_options; - session_options.SetEpContextDataReadFunc(LoadEpContextDataCallback, &compile_read_callback_state); + SetEpContextDataReadFunc(session_options, LoadEpContextDataCallback, &compile_read_callback_state); std::unordered_map ep_options; session_options.AppendExecutionProvider_V2(*ort_env, {plugin_ep_device}, ep_options); @@ -840,7 +851,7 @@ TEST(OrtEpLibrary, PluginEp_GenEpContextModel_EmbedModeDoesNotUseCallbacks) { compile_options.SetInputModelPath(input_model_file); compile_options.SetOutputModelPath(output_model_file); compile_options.SetEpContextEmbedMode(true); - compile_options.SetEpContextDataWriteFunc(StoreEpContextDataCallback, &write_callback_state); + SetEpContextDataWriteFunc(compile_options, StoreEpContextDataCallback, &write_callback_state); ASSERT_CXX_ORTSTATUS_OK(Ort::CompileModel(*ort_env, compile_options)); } @@ -869,7 +880,7 @@ TEST(OrtEpLibrary, PluginEp_GenEpContextModel_EmbedModeDoesNotUseCallbacks) { EpContextDataCallbackState load_read_callback_state; { Ort::SessionOptions session_options; - session_options.SetEpContextDataReadFunc(LoadEpContextDataCallback, &load_read_callback_state); + SetEpContextDataReadFunc(session_options, LoadEpContextDataCallback, &load_read_callback_state); std::unordered_map ep_options; session_options.AppendExecutionProvider_V2(*ort_env, {plugin_ep_device}, ep_options); @@ -971,7 +982,7 @@ TEST(OrtEpLibrary, PluginEp_GenEpContextModel_ExternalDataUsesWriteCallback) { compile_options.SetInputModelPath(input_model_file); compile_options.SetOutputModelPath(output_model_file); compile_options.SetEpContextEmbedMode(false); - compile_options.SetEpContextDataWriteFunc(StoreEpContextDataCallback, &callback_state); + SetEpContextDataWriteFunc(compile_options, StoreEpContextDataCallback, &callback_state); ASSERT_CXX_ORTSTATUS_OK(Ort::CompileModel(*ort_env, compile_options)); ASSERT_TRUE(std::filesystem::exists(output_model_file)); @@ -1001,7 +1012,7 @@ TEST(OrtEpLibrary, PluginEp_LoadEpContextModel_ExternalDataUsesReadCallback) { compile_options.SetInputModelPath(input_model_file); compile_options.SetOutputModelPath(compiled_model_file); compile_options.SetEpContextEmbedMode(false); - compile_options.SetEpContextDataWriteFunc(StoreEpContextDataCallback, &write_callback_state); + SetEpContextDataWriteFunc(compile_options, StoreEpContextDataCallback, &write_callback_state); ASSERT_CXX_ORTSTATUS_OK(Ort::CompileModel(*ort_env, compile_options)); ASSERT_TRUE(std::filesystem::exists(compiled_model_file)); @@ -1012,7 +1023,7 @@ TEST(OrtEpLibrary, PluginEp_LoadEpContextModel_ExternalDataUsesReadCallback) { read_callback_state.payload = write_callback_state.payload; { Ort::SessionOptions session_options; - session_options.SetEpContextDataReadFunc(LoadEpContextDataCallback, &read_callback_state); + SetEpContextDataReadFunc(session_options, LoadEpContextDataCallback, &read_callback_state); std::unordered_map ep_options; session_options.AppendExecutionProvider_V2(*ort_env, {plugin_ep_device}, ep_options); diff --git a/onnxruntime/test/framework/ep_plugin_provider_test.cc b/onnxruntime/test/framework/ep_plugin_provider_test.cc index e702445ff5795..2382e17337ec4 100644 --- a/onnxruntime/test/framework/ep_plugin_provider_test.cc +++ b/onnxruntime/test/framework/ep_plugin_provider_test.cc @@ -26,6 +26,7 @@ #include "core/session/abi_devices.h" #include "core/session/model_compilation_options.h" #include "core/session/onnxruntime_cxx_api.h" +#include "core/session/onnxruntime_experimental_c_api.h" #include "core/session/onnxruntime_session_options_config_keys.h" #include "test/util/include/api_asserts.h" #include "test/util/include/asserts.h" @@ -1775,24 +1776,32 @@ TEST(PluginExecutionProviderTest, GetGraphCaptureNodeAssignmentPolicy) { } TEST(PluginExecutionProviderTest, EpContextDataReadFuncIsReturnedByEpApi) { - const auto& ep_api = Ort::GetEpApi(); + const auto& ort_api = Ort::GetApi(); Ort::SessionOptions session_options; + auto set_read_func = Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_Fn(&ort_api); + auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&ort_api); + auto release_config_fn = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); + auto get_read_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&ort_api); + ASSERT_NE(set_read_func, nullptr); + ASSERT_NE(get_config, nullptr); + ASSERT_NE(release_config_fn, nullptr); + ASSERT_NE(get_read_func, nullptr); + EpContextReadCallbackState callback_state{ false, {}, {'e', 'p', 'c', 't', 'x'}, }; - session_options.SetEpContextDataReadFunc(EpContextReadCallback, &callback_state); + ASSERT_ORTSTATUS_OK(set_read_func(session_options, EpContextReadCallback, &callback_state)); OrtEpContextConfig* ep_context_config = nullptr; - ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(session_options, &ep_context_config)); - auto release_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(ep_context_config); }); + ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); + auto release_config = gsl::finally([&]() { release_config_fn(ep_context_config); }); OrtReadNamedBufferFunc read_func = nullptr; void* callback_state_out = nullptr; - ASSERT_ORTSTATUS_OK(ep_api.EpContextConfig_GetEpContextDataReadFunc(ep_context_config, &read_func, - &callback_state_out)); + ASSERT_ORTSTATUS_OK(get_read_func(ep_context_config, &read_func, &callback_state_out)); ASSERT_EQ(read_func, EpContextReadCallback); ASSERT_EQ(callback_state_out, &callback_state); @@ -1815,102 +1824,155 @@ TEST(PluginExecutionProviderTest, EpContextDataReadFuncIsReturnedByEpApi) { TEST(PluginExecutionProviderTest, EpContextDataApiRejectsInvalidArguments) { const auto& ort_api = Ort::GetApi(); - const auto& ep_api = Ort::GetEpApi(); + + auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&ort_api); + auto release_config_fn = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); + auto get_read_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&ort_api); + auto get_write_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); + auto set_read_func = Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_Fn(&ort_api); + ASSERT_NE(get_config, nullptr); + ASSERT_NE(release_config_fn, nullptr); + ASSERT_NE(get_read_func, nullptr); + ASSERT_NE(get_write_func, nullptr); + ASSERT_NE(set_read_func, nullptr); Ort::SessionOptions session_options; OrtEpContextConfig* ep_context_config = nullptr; - ExpectOrtStatus(ep_api.SessionOptions_GetEpContextConfig(nullptr, &ep_context_config), ORT_INVALID_ARGUMENT, - "OrtSessionOptions is NULL"); - ExpectOrtStatus(ep_api.SessionOptions_GetEpContextConfig(session_options, nullptr), ORT_INVALID_ARGUMENT, - "Output OrtEpContextConfig is NULL"); + ExpectOrtStatus(get_config(nullptr, &ep_context_config), ORT_INVALID_ARGUMENT, "OrtSessionOptions is NULL"); + ExpectOrtStatus(get_config(session_options, nullptr), ORT_INVALID_ARGUMENT, "Output OrtEpContextConfig is NULL"); - ExpectOrtStatus(ort_api.SessionOptions_SetEpContextDataReadFunc(nullptr, EpContextReadCallback, nullptr), + ExpectOrtStatus(set_read_func(nullptr, EpContextReadCallback, nullptr), ORT_INVALID_ARGUMENT, "'options' parameter must not be NULL"); - ExpectOrtStatus(ort_api.SessionOptions_SetEpContextDataReadFunc(session_options, nullptr, nullptr), + ExpectOrtStatus(set_read_func(session_options, nullptr, nullptr), ORT_INVALID_ARGUMENT, "'read_func' parameter must not be NULL"); - ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(session_options, &ep_context_config)); - auto release_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(ep_context_config); }); + ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); + auto release_config = gsl::finally([&]() { release_config_fn(ep_context_config); }); OrtReadNamedBufferFunc read_func = nullptr; OrtWriteNamedBufferFunc write_func = nullptr; void* state = nullptr; - ExpectOrtStatus(ep_api.EpContextConfig_GetEpContextDataReadFunc(nullptr, &read_func, &state), + ExpectOrtStatus(get_read_func(nullptr, &read_func, &state), ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); - ExpectOrtStatus(ep_api.EpContextConfig_GetEpContextDataReadFunc(ep_context_config, nullptr, &state), + ExpectOrtStatus(get_read_func(ep_context_config, nullptr, &state), ORT_INVALID_ARGUMENT, "Output read_func is NULL"); - ExpectOrtStatus(ep_api.EpContextConfig_GetEpContextDataReadFunc(ep_context_config, &read_func, nullptr), + ExpectOrtStatus(get_read_func(ep_context_config, &read_func, nullptr), ORT_INVALID_ARGUMENT, "Output state is NULL"); - ExpectOrtStatus(ep_api.EpContextConfig_GetEpContextDataWriteFunc(nullptr, &write_func, &state), + ExpectOrtStatus(get_write_func(nullptr, &write_func, &state), ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); - ExpectOrtStatus(ep_api.EpContextConfig_GetEpContextDataWriteFunc(ep_context_config, nullptr, &state), + ExpectOrtStatus(get_write_func(ep_context_config, nullptr, &state), ORT_INVALID_ARGUMENT, "Output write_func is NULL"); - ExpectOrtStatus(ep_api.EpContextConfig_GetEpContextDataWriteFunc(ep_context_config, &write_func, nullptr), + ExpectOrtStatus(get_write_func(ep_context_config, &write_func, nullptr), ORT_INVALID_ARGUMENT, "Output state is NULL"); #if !defined(ORT_MINIMAL_BUILD) Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "EpContextDataApiRejectsInvalidArguments"}; Ort::ModelCompilationOptions compilation_options{env, session_options}; - const auto& compile_api = Ort::GetCompileApi(); - ExpectOrtStatus(compile_api.ModelCompilationOptions_SetEpContextDataWriteFunc(nullptr, EpContextWriteCallback, - nullptr), + auto set_write_func = + Ort::Experimental::Get_OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); + ASSERT_NE(set_write_func, nullptr); + ExpectOrtStatus(set_write_func(nullptr, EpContextWriteCallback, nullptr), ORT_INVALID_ARGUMENT, "OrtModelCompilationOptions is NULL"); - ExpectOrtStatus(compile_api.ModelCompilationOptions_SetEpContextDataWriteFunc(compilation_options, nullptr, - nullptr), + ExpectOrtStatus(set_write_func(compilation_options, nullptr, nullptr), ORT_INVALID_ARGUMENT, "OrtWriteNamedBufferFunc function is NULL"); #endif // !defined(ORT_MINIMAL_BUILD) } TEST(PluginExecutionProviderTest, EpContextDataAccessorsReturnNullWhenCallbacksUnset) { - const auto& ep_api = Ort::GetEpApi(); + const auto& ort_api = Ort::GetApi(); Ort::SessionOptions session_options; + auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&ort_api); + auto release_config_fn = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); + auto get_read_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&ort_api); + auto get_write_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); + ASSERT_NE(get_config, nullptr); + ASSERT_NE(release_config_fn, nullptr); + ASSERT_NE(get_read_func, nullptr); + ASSERT_NE(get_write_func, nullptr); + OrtEpContextConfig* ep_context_config = nullptr; - ASSERT_ORTSTATUS_OK(ep_api.SessionOptions_GetEpContextConfig(session_options, &ep_context_config)); - auto release_config = gsl::finally([&]() { ep_api.ReleaseEpContextConfig(ep_context_config); }); + ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); + auto release_config = gsl::finally([&]() { release_config_fn(ep_context_config); }); OrtReadNamedBufferFunc read_func = EpContextReadCallback; OrtWriteNamedBufferFunc write_func = EpContextWriteCallback; void* state = reinterpret_cast(0x1); - ASSERT_ORTSTATUS_OK(ep_api.EpContextConfig_GetEpContextDataReadFunc(ep_context_config, &read_func, &state)); + ASSERT_ORTSTATUS_OK(get_read_func(ep_context_config, &read_func, &state)); EXPECT_EQ(read_func, nullptr); EXPECT_EQ(state, nullptr); - ASSERT_ORTSTATUS_OK(ep_api.EpContextConfig_GetEpContextDataWriteFunc(ep_context_config, &write_func, &state)); + ASSERT_ORTSTATUS_OK(get_write_func(ep_context_config, &write_func, &state)); EXPECT_EQ(write_func, nullptr); EXPECT_EQ(state, nullptr); } -TEST(PluginExecutionProviderTest, EpContextConfigCxxWrapperReturnsCallbacks) { +TEST(PluginExecutionProviderTest, EpContextConfigReturnsConfiguredCallbacks) { + const auto& ort_api = Ort::GetApi(); Ort::SessionOptions session_options; + auto set_read_func = Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_Fn(&ort_api); + auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&ort_api); + auto release_config_fn = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); + auto get_read_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&ort_api); + auto get_write_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); + ASSERT_NE(set_read_func, nullptr); + ASSERT_NE(get_config, nullptr); + ASSERT_NE(release_config_fn, nullptr); + ASSERT_NE(get_read_func, nullptr); + ASSERT_NE(get_write_func, nullptr); + EpContextReadCallbackState callback_state{}; - session_options.SetEpContextDataReadFunc(EpContextReadCallback, &callback_state); + ASSERT_ORTSTATUS_OK(set_read_func(session_options, EpContextReadCallback, &callback_state)); - Ort::EpContextConfig ep_context_config{session_options}; - auto [read_func, read_state] = ep_context_config.GetEpContextDataReadFunc(); + OrtEpContextConfig* ep_context_config = nullptr; + ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); + auto release_config = gsl::finally([&]() { release_config_fn(ep_context_config); }); + + OrtReadNamedBufferFunc read_func = nullptr; + void* read_state = nullptr; + ASSERT_ORTSTATUS_OK(get_read_func(ep_context_config, &read_func, &read_state)); EXPECT_EQ(read_func, EpContextReadCallback); EXPECT_EQ(read_state, &callback_state); - auto [write_func, write_state] = ep_context_config.GetEpContextDataWriteFunc(); + OrtWriteNamedBufferFunc write_func = nullptr; + void* write_state = nullptr; + ASSERT_ORTSTATUS_OK(get_write_func(ep_context_config, &write_func, &write_state)); EXPECT_EQ(write_func, nullptr); EXPECT_EQ(write_state, nullptr); } #if !defined(ORT_MINIMAL_BUILD) TEST(PluginExecutionProviderTest, EpContextDataWriteFuncIsReturnedByEpApi) { + const auto& ort_api = Ort::GetApi(); Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "EpContextDataWriteFuncIsReturnedByEpApi"}; Ort::SessionOptions session_options; Ort::ModelCompilationOptions compilation_options{env, session_options}; + auto set_write_func = + Ort::Experimental::Get_OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); + auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&ort_api); + auto release_config_fn = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); + auto get_write_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); + ASSERT_NE(set_write_func, nullptr); + ASSERT_NE(get_config, nullptr); + ASSERT_NE(release_config_fn, nullptr); + ASSERT_NE(get_write_func, nullptr); + EpContextWriteCallbackState callback_state{}; - compilation_options.SetEpContextDataWriteFunc(EpContextWriteCallback, &callback_state); + ASSERT_ORTSTATUS_OK(set_write_func(compilation_options, EpContextWriteCallback, &callback_state)); const auto* internal_options = reinterpret_cast( static_cast(compilation_options)); - Ort::EpContextConfig ep_context_config{&internal_options->GetSessionOptions()}; - auto [write_func, callback_state_out] = ep_context_config.GetEpContextDataWriteFunc(); + + OrtEpContextConfig* ep_context_config = nullptr; + ASSERT_ORTSTATUS_OK(get_config(&internal_options->GetSessionOptions(), &ep_context_config)); + auto release_config = gsl::finally([&]() { release_config_fn(ep_context_config); }); + + OrtWriteNamedBufferFunc write_func = nullptr; + void* callback_state_out = nullptr; + ASSERT_ORTSTATUS_OK(get_write_func(ep_context_config, &write_func, &callback_state_out)); ASSERT_EQ(write_func, EpContextWriteCallback); ASSERT_EQ(callback_state_out, &callback_state); @@ -1924,13 +1986,28 @@ TEST(PluginExecutionProviderTest, EpContextDataWriteFuncIsReturnedByEpApi) { #endif // !defined(ORT_MINIMAL_BUILD) TEST(PluginExecutionProviderTest, EpContextDataReturnedReadFuncAllowsEmptyPayloads) { + const auto& ort_api = Ort::GetApi(); Ort::SessionOptions session_options; + auto set_read_func = Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_Fn(&ort_api); + auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&ort_api); + auto release_config_fn = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); + auto get_read_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&ort_api); + ASSERT_NE(set_read_func, nullptr); + ASSERT_NE(get_config, nullptr); + ASSERT_NE(release_config_fn, nullptr); + ASSERT_NE(get_read_func, nullptr); + EpContextReadCallbackState callback_state{}; - session_options.SetEpContextDataReadFunc(EpContextReadCallback, &callback_state); + ASSERT_ORTSTATUS_OK(set_read_func(session_options, EpContextReadCallback, &callback_state)); - Ort::EpContextConfig ep_context_config{session_options}; - auto [read_func, read_state] = ep_context_config.GetEpContextDataReadFunc(); + OrtEpContextConfig* ep_context_config = nullptr; + ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); + auto release_config = gsl::finally([&]() { release_config_fn(ep_context_config); }); + + OrtReadNamedBufferFunc read_func = nullptr; + void* read_state = nullptr; + ASSERT_ORTSTATUS_OK(get_read_func(ep_context_config, &read_func, &read_state)); ASSERT_EQ(read_func, EpContextReadCallback); ASSERT_EQ(read_state, &callback_state); From b86f1e7fc04df73e37f2ef4da4c9264acd087602 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Wed, 10 Jun 2026 18:36:44 -0700 Subject: [PATCH 18/48] Harden EPContext helper path validation --- .../autoep/library/ep_context_data_utils.h | 54 +++++++++++++++++-- onnxruntime/test/autoep/test_execution.cc | 18 +++++++ 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/onnxruntime/test/autoep/library/ep_context_data_utils.h b/onnxruntime/test/autoep/library/ep_context_data_utils.h index 007adf2f2696b..77cc6377f4156 100644 --- a/onnxruntime/test/autoep/library/ep_context_data_utils.h +++ b/onnxruntime/test/autoep/library/ep_context_data_utils.h @@ -78,6 +78,16 @@ inline std::string PathToUtf8String(const std::filesystem::path& path) { #endif } +inline bool ContainsPathTraversal(const std::filesystem::path& path) { + const std::filesystem::path parent_dir{".."}; + for (const auto& component : path) { + if (component == parent_dir) { + return true; + } + } + return false; +} + // NOTE: This sample resolves file_name (which can originate from an untrusted EPContext model's // "ep_cache_context" attribute) directly into a filesystem path. An absolute path or one containing ".." // segments can therefore escape the model directory. Production EPs that adopt this pattern should validate @@ -91,18 +101,54 @@ inline OrtStatus* ResolveEpContextDataPath(const OrtApi& api, const char* file_n return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); } + const auto candidate_path = Utf8Path(file_name); + if (candidate_path.empty()) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name is not a valid path"); + } + + if (candidate_path.is_absolute()) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute"); + } + + if (ContainsPathTraversal(candidate_path)) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not contain path traversal"); + } + + data_path = candidate_path; + if (graph == nullptr) { + return nullptr; + } + + const ORTCHAR_T* model_path = nullptr; + RETURN_IF_ERROR(api.Graph_GetModelPath(graph, &model_path)); + if (model_path == nullptr || model_path[0] == 0) { + return nullptr; + } + + data_path = std::filesystem::path{model_path}.parent_path() / data_path; + return nullptr; +} + +inline OrtStatus* ResolveEpContextDataFilePath(const OrtApi& api, const char* file_name, const OrtGraph* graph, + std::filesystem::path& data_path) { + data_path.clear(); + + if (file_name == nullptr || file_name[0] == '\0') { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); + } + data_path = Utf8Path(file_name); if (data_path.empty()) { return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name is not a valid path"); } - if (data_path.is_absolute() || graph == nullptr) { + if (graph == nullptr) { return nullptr; } const ORTCHAR_T* model_path = nullptr; RETURN_IF_ERROR(api.Graph_GetModelPath(graph, &model_path)); - if (model_path == nullptr || model_path[0] == 0) { + if (model_path == nullptr || model_path[0] == 0 || data_path.is_absolute()) { return nullptr; } @@ -115,7 +161,7 @@ inline OrtStatus* ReadEpContextDataFromFile(const OrtApi& api, const char* file_ data.clear(); std::filesystem::path data_path; - RETURN_IF_ERROR(ResolveEpContextDataPath(api, file_name, graph, data_path)); + RETURN_IF_ERROR(ResolveEpContextDataFilePath(api, file_name, graph, data_path)); std::ifstream input_stream(data_path, std::ios::binary); if (!input_stream) { @@ -141,7 +187,7 @@ inline OrtStatus* WriteEpContextDataToFile(const OrtApi& api, const char* file_n } std::filesystem::path data_path; - RETURN_IF_ERROR(ResolveEpContextDataPath(api, file_name, graph, data_path)); + RETURN_IF_ERROR(ResolveEpContextDataFilePath(api, file_name, graph, data_path)); std::ofstream output_stream(data_path, std::ios::binary); if (!output_stream) { diff --git a/onnxruntime/test/autoep/test_execution.cc b/onnxruntime/test/autoep/test_execution.cc index 3be1f8fd52458..eabc92f570328 100644 --- a/onnxruntime/test/autoep/test_execution.cc +++ b/onnxruntime/test/autoep/test_execution.cc @@ -690,6 +690,24 @@ TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathAndInvalidArguments) { ORT_INVALID_ARGUMENT, "EPContext data fallback file name must not be empty"); } +TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathRejectsUnsafeNames) { + const auto& api = Ort::GetApi(); + std::filesystem::path data_path; + + ExpectOrtStatusError(ep_context_data_utils::ResolveEpContextDataPath(api, "../escape.ctx", nullptr, data_path), + ORT_INVALID_ARGUMENT, "EPContext data file name must not contain path traversal"); + EXPECT_TRUE(data_path.empty()); + +#ifdef _WIN32 + const char* absolute_file_name = "C:\\temp\\escape.ctx"; +#else + const char* absolute_file_name = "/tmp/escape.ctx"; +#endif + ExpectOrtStatusError(ep_context_data_utils::ResolveEpContextDataPath(api, absolute_file_name, nullptr, data_path), + ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute"); + EXPECT_TRUE(data_path.empty()); +} + TEST(OrtEpLibrary, EpContextDataUtils_FileFallbackReadsAndWrites) { const auto& api = Ort::GetApi(); const std::filesystem::path test_dir = PrepareTempTestDir("ort_ep_context_data_utils_file_fallback_test"); From da105b2040181994fb24641f4e2c374d40e18851 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Thu, 11 Jun 2026 19:19:29 -0700 Subject: [PATCH 19/48] Address PR review feedback on EPContext callback APIs - example EP: drop unnecessary (void) cast on ep_context_data output var - example EP: require "1" for ep_context_embed_mode (consistent boolean parsing) - ep_context_options.h: document why the experimental header include is needed - experimental C API: allow clearing the EPContext read callback via nullptr (also clears state); update docs and add EpContextDataReadFuncCanBeCleared test - experimental C API docs: clarify callbacks may return any OrtErrorCode, not only ORT_FAIL - test: compare spans via std::equal instead of copying into a temporary vector --- .../session/onnxruntime_experimental_c_api.h | 8 ++-- .../onnxruntime_experimental_c_api.inc | 7 ++-- .../core/framework/ep_context_options.h | 2 + .../core/session/experimental_c_api.cc | 7 ++-- .../autoep/library/example_plugin_ep/ep.cc | 1 - .../library/example_plugin_ep/ep_factory.cc | 2 +- .../test/framework/ep_plugin_provider_test.cc | 39 +++++++++++++++++-- 7 files changed, 50 insertions(+), 16 deletions(-) diff --git a/include/onnxruntime/core/session/onnxruntime_experimental_c_api.h b/include/onnxruntime/core/session/onnxruntime_experimental_c_api.h index 0d06a38c1ff69..3f74832bb89cd 100644 --- a/include/onnxruntime/core/session/onnxruntime_experimental_c_api.h +++ b/include/onnxruntime/core/session/onnxruntime_experimental_c_api.h @@ -71,8 +71,8 @@ ORT_RUNTIME_CLASS(EpContextConfig); * \param[in] buffer_num_bytes The size of the buffer in bytes. * * \return OrtStatus* Write status. Return nullptr on success. - * Use CreateStatus to provide error info with ORT_FAIL as the error code. - * ORT will release the OrtStatus* if not null. + * On failure, use CreateStatus to provide error info with an appropriate OrtErrorCode + * (e.g., ORT_FAIL); ORT propagates the returned code. ORT will release the OrtStatus* if not null. */ typedef OrtStatus*(ORT_API_CALL* OrtWriteNamedBufferFunc)(_In_ void* state, _In_ const char* name, @@ -96,8 +96,8 @@ typedef OrtStatus*(ORT_API_CALL* OrtWriteNamedBufferFunc)(_In_ void* state, * \param[out] data_size Set by the implementation to the size of the output data in bytes. * * \return OrtStatus* Read status. Return nullptr on success. - * Use CreateStatus to provide error info with ORT_FAIL as the error code. - * ORT will release the OrtStatus* if not null. + * On failure, use CreateStatus to provide error info with an appropriate OrtErrorCode + * (e.g., ORT_FAIL); ORT propagates the returned code. ORT will release the OrtStatus* if not null. */ typedef OrtStatus*(ORT_API_CALL* OrtReadNamedBufferFunc)(_In_ void* state, _In_ const char* name, diff --git a/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc b/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc index a05f64ee810a7..cf17b1509a189 100644 --- a/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc +++ b/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc @@ -297,13 +297,14 @@ ORT_EXPERIMENTAL_API(28, OrtStatusPtr, OrtModelPackageApi_CreateSession, * is responsible for synchronization. * * \param[in] options The OrtSessionOptions instance. - * \param[in] read_func The OrtReadNamedBufferFunc callback. - * \param[in] state Opaque state passed to read_func. Can be NULL. + * \param[in] read_func The OrtReadNamedBufferFunc callback. Pass NULL to clear a previously set callback (any + * previously set state is cleared as well). + * \param[in] state Opaque state passed to read_func. Can be NULL. Ignored when read_func is NULL. * * \snippet{doc} snippets.dox OrtStatus Return Value */ ORT_EXPERIMENTAL_API(28, OrtStatusPtr, OrtApi_SessionOptions_SetEpContextDataReadFunc, - _Inout_ OrtSessionOptions* options, _In_ OrtReadNamedBufferFunc read_func, _In_opt_ void* state) + _Inout_ OrtSessionOptions* options, _In_opt_ OrtReadNamedBufferFunc read_func, _In_opt_ void* state) /** \brief Sets a callback for writing EPContext binary data during compilation. * diff --git a/onnxruntime/core/framework/ep_context_options.h b/onnxruntime/core/framework/ep_context_options.h index 29fa796272c9f..344ec5f7f6b58 100644 --- a/onnxruntime/core/framework/ep_context_options.h +++ b/onnxruntime/core/framework/ep_context_options.h @@ -7,6 +7,8 @@ #include #include "core/framework/allocator.h" #include "core/framework/config_options.h" +// Needed for OrtWriteNamedBufferFunc (used by EpContextDataWriteFuncHolder below). This include can be removed +// once the experimental EPContext data callback APIs are promoted to the stable C API. #include "core/session/onnxruntime_experimental_c_api.h" namespace onnxruntime { diff --git a/onnxruntime/core/session/experimental_c_api.cc b/onnxruntime/core/session/experimental_c_api.cc index b2c31f2bc6b4e..ad522e0326e1e 100644 --- a/onnxruntime/core/session/experimental_c_api.cc +++ b/onnxruntime/core/session/experimental_c_api.cc @@ -58,13 +58,14 @@ ORT_API_STATUS_IMPL(OrtApi_ExperimentalApiTest_SinceV28, } ORT_API_STATUS_IMPL(OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28, _Inout_ OrtSessionOptions* options, - _In_ OrtReadNamedBufferFunc read_func, _In_opt_ void* state) { + _In_opt_ OrtReadNamedBufferFunc read_func, _In_opt_ void* state) { API_IMPL_BEGIN ORT_API_RETURN_IF(options == nullptr, ORT_INVALID_ARGUMENT, "'options' parameter must not be NULL"); - ORT_API_RETURN_IF(read_func == nullptr, ORT_INVALID_ARGUMENT, "'read_func' parameter must not be NULL"); + // Passing a null read_func clears any previously set callback. Clear the state too so a stale state pointer is + // never paired with a missing callback. options->value.ep_context_data_read_func = read_func; - options->value.ep_context_data_read_state = state; + options->value.ep_context_data_read_state = read_func != nullptr ? state : nullptr; return nullptr; API_IMPL_END } diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc index 8c5cff4ae1c0b..83e38174bdc8b 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc @@ -436,7 +436,6 @@ OrtStatus* ORT_API_CALL ExampleEp::CompileImpl(_In_ OrtEp* this_ptr, _In_ const Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&ep->ort_api), ep->ep_context_config_, ep_cache_context.c_str(), ort_graphs[0], ep_context_data)); - (void)ep_context_data; } // Create EpContextKernel for EPContext nodes - clearly separates from MulKernel diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc index b1890b6996763..4d45c6394ad71 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc @@ -233,7 +233,7 @@ OrtStatus* ORT_API_CALL ExampleEpFactory::CreateEpImpl(OrtEpFactory* this_ptr, ExampleEp::Config config = {}; config.enable_ep_context = ep_context_enable == "1"; - config.embed_ep_context_in_model = ep_context_embed_mode != "0"; + config.embed_ep_context_in_model = ep_context_embed_mode == "1"; config.ep_context_output_model_path = std::move(ep_context_output_model_path); config.enable_weightless_ep_context_nodes = weightless_ep_context_nodes_enable == "1"; diff --git a/onnxruntime/test/framework/ep_plugin_provider_test.cc b/onnxruntime/test/framework/ep_plugin_provider_test.cc index 2382e17337ec4..aa16c7e83ae34 100644 --- a/onnxruntime/test/framework/ep_plugin_provider_test.cc +++ b/onnxruntime/test/framework/ep_plugin_provider_test.cc @@ -1818,8 +1818,8 @@ TEST(PluginExecutionProviderTest, EpContextDataReadFuncIsReturnedByEpApi) { ASSERT_TRUE(callback_state.called); EXPECT_EQ(callback_state.file_name, "context.bin"); ASSERT_EQ(buffer_size, callback_state.payload.size()); - EXPECT_EQ(std::vector(static_cast(buffer), static_cast(buffer) + buffer_size), - callback_state.payload); + EXPECT_TRUE(std::equal(callback_state.payload.begin(), callback_state.payload.end(), + static_cast(buffer))); } TEST(PluginExecutionProviderTest, EpContextDataApiRejectsInvalidArguments) { @@ -1843,8 +1843,8 @@ TEST(PluginExecutionProviderTest, EpContextDataApiRejectsInvalidArguments) { ExpectOrtStatus(set_read_func(nullptr, EpContextReadCallback, nullptr), ORT_INVALID_ARGUMENT, "'options' parameter must not be NULL"); - ExpectOrtStatus(set_read_func(session_options, nullptr, nullptr), - ORT_INVALID_ARGUMENT, "'read_func' parameter must not be NULL"); + // A null read_func is allowed: it clears any previously set callback (covered by + // EpContextDataReadFuncCanBeCleared), so it is not rejected here. ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); auto release_config = gsl::finally([&]() { release_config_fn(ep_context_config); }); @@ -1943,6 +1943,37 @@ TEST(PluginExecutionProviderTest, EpContextConfigReturnsConfiguredCallbacks) { EXPECT_EQ(write_state, nullptr); } +TEST(PluginExecutionProviderTest, EpContextDataReadFuncCanBeCleared) { + const auto& ort_api = Ort::GetApi(); + Ort::SessionOptions session_options; + + auto set_read_func = Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_Fn(&ort_api); + auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&ort_api); + auto release_config_fn = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); + auto get_read_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&ort_api); + ASSERT_NE(set_read_func, nullptr); + ASSERT_NE(get_config, nullptr); + ASSERT_NE(release_config_fn, nullptr); + ASSERT_NE(get_read_func, nullptr); + + EpContextReadCallbackState callback_state{}; + ASSERT_ORTSTATUS_OK(set_read_func(session_options, EpContextReadCallback, &callback_state)); + + // Passing a null read_func clears the callback. The previously set state must also be cleared so a stale state + // pointer is never paired with a missing callback. + ASSERT_ORTSTATUS_OK(set_read_func(session_options, nullptr, &callback_state)); + + OrtEpContextConfig* ep_context_config = nullptr; + ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); + auto release_config = gsl::finally([&]() { release_config_fn(ep_context_config); }); + + OrtReadNamedBufferFunc read_func = EpContextReadCallback; + void* read_state = reinterpret_cast(0x1); + ASSERT_ORTSTATUS_OK(get_read_func(ep_context_config, &read_func, &read_state)); + EXPECT_EQ(read_func, nullptr); + EXPECT_EQ(read_state, nullptr); +} + #if !defined(ORT_MINIMAL_BUILD) TEST(PluginExecutionProviderTest, EpContextDataWriteFuncIsReturnedByEpApi) { const auto& ort_api = Ort::GetApi(); From 689e3e5fc5946da9414c37027db1ddad9b662905 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Thu, 11 Jun 2026 20:30:38 -0700 Subject: [PATCH 20/48] Address EPContext callback review follow-ups --- .../core/session/model_compilation_options.cc | 11 ++ .../autoep/library/ep_context_data_utils.h | 153 +++++++++++++----- .../autoep/library/example_plugin_ep/ep.cc | 8 +- .../library/example_plugin_ep/ep_factory.cc | 10 +- onnxruntime/test/autoep/test_execution.cc | 72 ++++++--- 5 files changed, 188 insertions(+), 66 deletions(-) diff --git a/onnxruntime/core/session/model_compilation_options.cc b/onnxruntime/core/session/model_compilation_options.cc index 32dd4a4a77e9c..294afadc42786 100644 --- a/onnxruntime/core/session/model_compilation_options.cc +++ b/onnxruntime/core/session/model_compilation_options.cc @@ -333,6 +333,17 @@ Status ModelCompilationOptions::Check() const { "of the output model (e.g., file, buffer, or stream) is not specified."); } + const bool output_model_is_file = ep_context_gen_options.TryGetOutputModelPath() != nullptr; + const bool has_ep_context_data_write_func = ep_context_gen_options.TryGetEpContextDataWriteFunc() != nullptr; + const bool has_ep_context_file_path = + !config_options.GetConfigOrDefault(kOrtSessionOptionEpContextFilePath, "").empty(); + if (!ep_context_gen_options.embed_ep_context_in_model && !output_model_is_file && !has_ep_context_file_path && + !has_ep_context_data_write_func) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "External EPContext data requires an output model path or an " + "OrtWriteNamedBufferFunc callback"); + } + const epctx::BufferHolder* output_buffer_ptr = ep_context_gen_options.TryGetOutputModelBuffer(); if (output_buffer_ptr != nullptr && output_buffer_ptr->buffer_ptr == nullptr) { diff --git a/onnxruntime/test/autoep/library/ep_context_data_utils.h b/onnxruntime/test/autoep/library/ep_context_data_utils.h index 77cc6377f4156..a9dc225f832ae 100644 --- a/onnxruntime/test/autoep/library/ep_context_data_utils.h +++ b/onnxruntime/test/autoep/library/ep_context_data_utils.h @@ -88,14 +88,9 @@ inline bool ContainsPathTraversal(const std::filesystem::path& path) { return false; } -// NOTE: This sample resolves file_name (which can originate from an untrusted EPContext model's -// "ep_cache_context" attribute) directly into a filesystem path. An absolute path or one containing ".." -// segments can therefore escape the model directory. Production EPs that adopt this pattern should validate -// or sandbox the resolved path (e.g. reject absolute paths and traversal, confine to an allowed directory) -// and bound the amount of data read. -inline OrtStatus* ResolveEpContextDataPath(const OrtApi& api, const char* file_name, const OrtGraph* graph, - std::filesystem::path& data_path) { - data_path.clear(); +inline OrtStatus* ValidateEpContextDataName(const OrtApi& api, const char* file_name, + std::filesystem::path& data_name) { + data_name.clear(); if (file_name == nullptr || file_name[0] == '\0') { return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); @@ -114,11 +109,39 @@ inline OrtStatus* ResolveEpContextDataPath(const OrtApi& api, const char* file_n return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not contain path traversal"); } + data_name = candidate_path; + return nullptr; +} + +// Resolve file_name (which can originate from an untrusted EPContext model's "ep_cache_context" attribute) as a +// model-relative path when graph is available. Trusted direct callers may pass an absolute path when graph is null. +// Production EPs should still apply their own sandboxing and size limits. +inline OrtStatus* ResolveEpContextDataPath(const OrtApi& api, const char* file_name, const OrtGraph* graph, + std::filesystem::path& data_path) { + data_path.clear(); + + if (file_name == nullptr || file_name[0] == '\0') { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); + } + + const auto candidate_path = Utf8Path(file_name); + if (candidate_path.empty()) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name is not a valid path"); + } + + if (ContainsPathTraversal(candidate_path)) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not contain path traversal"); + } + data_path = candidate_path; if (graph == nullptr) { return nullptr; } + if (candidate_path.is_absolute()) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute"); + } + const ORTCHAR_T* model_path = nullptr; RETURN_IF_ERROR(api.Graph_GetModelPath(graph, &model_path)); if (model_path == nullptr || model_path[0] == 0) { @@ -129,8 +152,8 @@ inline OrtStatus* ResolveEpContextDataPath(const OrtApi& api, const char* file_n return nullptr; } -inline OrtStatus* ResolveEpContextDataFilePath(const OrtApi& api, const char* file_name, const OrtGraph* graph, - std::filesystem::path& data_path) { +inline OrtStatus* ResolveEpContextDataOutputPath(const OrtApi& api, const char* file_name, const OrtGraph* graph, + std::filesystem::path& data_path) { data_path.clear(); if (file_name == nullptr || file_name[0] == '\0') { @@ -142,13 +165,17 @@ inline OrtStatus* ResolveEpContextDataFilePath(const OrtApi& api, const char* fi return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name is not a valid path"); } - if (graph == nullptr) { + if (ContainsPathTraversal(data_path)) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not contain path traversal"); + } + + if (graph == nullptr || data_path.is_absolute()) { return nullptr; } const ORTCHAR_T* model_path = nullptr; RETURN_IF_ERROR(api.Graph_GetModelPath(graph, &model_path)); - if (model_path == nullptr || model_path[0] == 0 || data_path.is_absolute()) { + if (model_path == nullptr || model_path[0] == 0) { return nullptr; } @@ -156,12 +183,37 @@ inline OrtStatus* ResolveEpContextDataFilePath(const OrtApi& api, const char* fi return nullptr; } +inline OrtStatus* WriteEpContextDataToResolvedFile(const OrtApi& api, const std::filesystem::path& data_path, + const void* buffer, size_t buffer_size) { + std::ofstream output_stream(data_path, std::ios::binary); + if (!output_stream) { + const std::string message = "Failed to open EPContext data file for write: " + + PathToUtf8String(data_path); + return api.CreateStatus(ORT_FAIL, message.c_str()); + } + + if (buffer_size != 0) { + if (buffer_size > static_cast(std::numeric_limits::max())) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data buffer is too large to write"); + } + + output_stream.write(static_cast(buffer), static_cast(buffer_size)); + if (!output_stream) { + const std::string message = "Failed to write EPContext data file: " + + PathToUtf8String(data_path); + return api.CreateStatus(ORT_FAIL, message.c_str()); + } + } + + return nullptr; +} + inline OrtStatus* ReadEpContextDataFromFile(const OrtApi& api, const char* file_name, const OrtGraph* graph, std::vector& data) { data.clear(); std::filesystem::path data_path; - RETURN_IF_ERROR(ResolveEpContextDataFilePath(api, file_name, graph, data_path)); + RETURN_IF_ERROR(ResolveEpContextDataPath(api, file_name, graph, data_path)); std::ifstream input_stream(data_path, std::ios::binary); if (!input_stream) { @@ -187,29 +239,8 @@ inline OrtStatus* WriteEpContextDataToFile(const OrtApi& api, const char* file_n } std::filesystem::path data_path; - RETURN_IF_ERROR(ResolveEpContextDataFilePath(api, file_name, graph, data_path)); - - std::ofstream output_stream(data_path, std::ios::binary); - if (!output_stream) { - const std::string message = "Failed to open EPContext data file for write: " + - PathToUtf8String(data_path); - return api.CreateStatus(ORT_FAIL, message.c_str()); - } - - if (buffer_size != 0) { - if (buffer_size > static_cast(std::numeric_limits::max())) { - return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data buffer is too large to write"); - } - - output_stream.write(static_cast(buffer), static_cast(buffer_size)); - if (!output_stream) { - const std::string message = "Failed to write EPContext data file: " + - PathToUtf8String(data_path); - return api.CreateStatus(ORT_FAIL, message.c_str()); - } - } - - return nullptr; + RETURN_IF_ERROR(ResolveEpContextDataPath(api, file_name, graph, data_path)); + return WriteEpContextDataToResolvedFile(api, data_path, buffer, buffer_size); } inline OrtStatus* ReadEpContextDataWithFileFallback( @@ -224,7 +255,11 @@ inline OrtStatus* ReadEpContextDataWithFileFallback( OrtReadNamedBufferFunc read_func = nullptr; void* read_state = nullptr; - if (get_read_func != nullptr && ep_context_config != nullptr) { + if (ep_context_config != nullptr && get_read_func == nullptr) { + return api.CreateStatus(ORT_NOT_IMPLEMENTED, + "OrtEpApi_EpContextConfig_GetEpContextDataReadFunc is not available"); + } + if (ep_context_config != nullptr) { RETURN_IF_ERROR(get_read_func(ep_context_config, &read_func, &read_state)); } @@ -261,6 +296,17 @@ inline OrtStatus* ReadEpContextDataWithFileFallback( return nullptr; } +inline OrtStatus* ReadEpContextDataWithFileFallback( + const OrtApi& api, + const OrtEpContextConfig* ep_context_config, + const char* file_name, const OrtGraph* graph, + std::vector& data) { + return ReadEpContextDataWithFileFallback( + api, + Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&api), + ep_context_config, file_name, graph, data); +} + inline OrtStatus* WriteEpContextDataWithFileFallback( const OrtApi& api, OrtExperimental_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn get_write_func, @@ -278,7 +324,11 @@ inline OrtStatus* WriteEpContextDataWithFileFallback( OrtWriteNamedBufferFunc write_func = nullptr; void* write_state = nullptr; - if (get_write_func != nullptr && ep_context_config != nullptr) { + if (ep_context_config != nullptr && get_write_func == nullptr) { + return api.CreateStatus(ORT_NOT_IMPLEMENTED, + "OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc is not available"); + } + if (ep_context_config != nullptr) { RETURN_IF_ERROR(get_write_func(ep_context_config, &write_func, &write_state)); } @@ -286,11 +336,28 @@ inline OrtStatus* WriteEpContextDataWithFileFallback( return write_func(write_state, file_name, buffer, buffer_size); } + std::filesystem::path logical_path; + RETURN_IF_ERROR(ValidateEpContextDataName(api, file_name, logical_path)); + if (fallback_file_name == nullptr || fallback_file_name[0] == '\0') { return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data fallback file name must not be empty"); } - return WriteEpContextDataToFile(api, fallback_file_name, graph, buffer, buffer_size); + std::filesystem::path data_path; + RETURN_IF_ERROR(ResolveEpContextDataOutputPath(api, fallback_file_name, graph, data_path)); + return WriteEpContextDataToResolvedFile(api, data_path, buffer, buffer_size); +} + +inline OrtStatus* WriteEpContextDataWithFileFallback( + const OrtApi& api, + const OrtEpContextConfig* ep_context_config, + const char* file_name, const char* fallback_file_name, + const OrtGraph* graph, + const void* buffer, size_t buffer_size) { + return WriteEpContextDataWithFileFallback( + api, + Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn(&api), + ep_context_config, file_name, fallback_file_name, graph, buffer, buffer_size); } inline OrtStatus* WriteEpContextDataWithFileFallback( @@ -303,4 +370,12 @@ inline OrtStatus* WriteEpContextDataWithFileFallback( buffer_size); } +inline OrtStatus* WriteEpContextDataWithFileFallback( + const OrtApi& api, + const OrtEpContextConfig* ep_context_config, + const char* file_name, const OrtGraph* graph, + const void* buffer, size_t buffer_size) { + return WriteEpContextDataWithFileFallback(api, ep_context_config, file_name, file_name, graph, buffer, buffer_size); +} + } // namespace ep_context_data_utils diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc index 83e38174bdc8b..332d716401daf 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc @@ -432,9 +432,7 @@ OrtStatus* ORT_API_CALL ExampleEp::CompileImpl(_In_ OrtEp* this_ptr, _In_ const std::vector ep_context_data; RETURN_IF_ERROR(ep_context_data_utils::ReadEpContextDataWithFileFallback( - ep->ort_api, - Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&ep->ort_api), - ep->ep_context_config_, ep_cache_context.c_str(), ort_graphs[0], + ep->ort_api, ep->ep_context_config_, ep_cache_context.c_str(), ort_graphs[0], ep_context_data)); } @@ -562,9 +560,7 @@ OrtStatus* ExampleEp::CreateEpContextNodes(const OrtGraph* graph, fallback_graph = nullptr; } RETURN_IF_ERROR(ep_context_data_utils::WriteEpContextDataWithFileFallback( - ort_api, - Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn(&ort_api), - ep_context_config_, ep_ctx.c_str(), fallback_ep_ctx.c_str(), fallback_graph, + ort_api, ep_context_config_, ep_ctx.c_str(), fallback_ep_ctx.c_str(), fallback_graph, ep_context_data.data(), ep_context_data.size())); } attributes[0] = Ort::OpAttr("ep_cache_context", ep_ctx.data(), static_cast(ep_ctx.size()), diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc index 4d45c6394ad71..299e88c251502 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc @@ -238,10 +238,14 @@ OrtStatus* ORT_API_CALL ExampleEpFactory::CreateEpImpl(OrtEpFactory* this_ptr, config.enable_weightless_ep_context_nodes = weightless_ep_context_nodes_enable == "1"; OrtEpContextConfig* ep_context_config = nullptr; - if (auto* get_ep_context_config = - Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&factory->ort_api)) { - RETURN_IF_ERROR(get_ep_context_config(session_options, &ep_context_config)); + auto* get_ep_context_config = + Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&factory->ort_api); + if (get_ep_context_config == nullptr) { + return factory->ort_api.CreateStatus(ORT_NOT_IMPLEMENTED, + "OrtEpApi_SessionOptions_GetEpContextConfig is not available"); } + RETURN_IF_ERROR(get_ep_context_config(session_options, &ep_context_config)); + auto dummy_ep = std::make_unique(*factory, factory->ep_name_, config, *logger, ep_context_config); diff --git a/onnxruntime/test/autoep/test_execution.cc b/onnxruntime/test/autoep/test_execution.cc index eabc92f570328..a80a8888d1c55 100644 --- a/onnxruntime/test/autoep/test_execution.cc +++ b/onnxruntime/test/autoep/test_execution.cc @@ -94,7 +94,7 @@ void ExpectOrtStatusError(OrtStatus* status_ptr, OrtErrorCode expected_code, std } std::filesystem::path PrepareTempTestDir(std::string_view name) { - std::filesystem::path test_dir = std::filesystem::temp_directory_path() / std::string{name}; + std::filesystem::path test_dir = std::string{name}; std::filesystem::remove_all(test_dir); std::filesystem::create_directories(test_dir); return test_dir; @@ -672,21 +672,17 @@ TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathAndInvalidArguments) { ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataToFile(api, "unused.ctx", nullptr, nullptr, 1), ORT_INVALID_ARGUMENT, "EPContext data buffer must not be null for non-empty data"); - ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback(api, FakeEpContextConfigGetWriteFunc, - nullptr, "unused.ctx", nullptr, + ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback(api, nullptr, "unused.ctx", nullptr, nullptr, 1), ORT_INVALID_ARGUMENT, "EPContext data buffer must not be null for non-empty data"); std::vector data; - ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataWithFileFallback(api, FakeEpContextConfigGetReadFunc, - nullptr, "", nullptr, data), + ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataWithFileFallback(api, nullptr, "", nullptr, data), ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); - ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback(api, FakeEpContextConfigGetWriteFunc, - nullptr, "", nullptr, nullptr, 0), + ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback(api, nullptr, "", nullptr, nullptr, 0), ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback( - api, FakeEpContextConfigGetWriteFunc, nullptr, "logical_context_data.bin", "", nullptr, - nullptr, 0), + api, nullptr, "logical_context_data.bin", "", nullptr, nullptr, 0), ORT_INVALID_ARGUMENT, "EPContext data fallback file name must not be empty"); } @@ -703,9 +699,15 @@ TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathRejectsUnsafeNames) { #else const char* absolute_file_name = "/tmp/escape.ctx"; #endif - ExpectOrtStatusError(ep_context_data_utils::ResolveEpContextDataPath(api, absolute_file_name, nullptr, data_path), + ASSERT_ORTSTATUS_OK(ep_context_data_utils::ResolveEpContextDataPath(api, absolute_file_name, nullptr, data_path)); + EXPECT_TRUE(data_path.is_absolute()); + + std::vector data; + ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataFromFile(api, "../escape.ctx", nullptr, data), + ORT_INVALID_ARGUMENT, "EPContext data file name must not contain path traversal"); + ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback( + api, nullptr, absolute_file_name, "unused.ctx", nullptr, nullptr, 0), ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute"); - EXPECT_TRUE(data_path.empty()); } TEST(OrtEpLibrary, EpContextDataUtils_FileFallbackReadsAndWrites) { @@ -727,19 +729,18 @@ TEST(OrtEpLibrary, EpContextDataUtils_FileFallbackReadsAndWrites) { const std::filesystem::path wrapper_data_path = test_dir / "wrapper_context_data.bin"; const std::string wrapper_data_file_name = ep_context_data_utils::PathToUtf8String(wrapper_data_path); ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( - api, FakeEpContextConfigGetWriteFunc, nullptr, wrapper_data_file_name.c_str(), nullptr, payload.data(), - payload.size())); + api, nullptr, wrapper_data_file_name.c_str(), nullptr, payload.data(), payload.size())); data.clear(); ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataWithFileFallback( - api, FakeEpContextConfigGetReadFunc, nullptr, wrapper_data_file_name.c_str(), nullptr, data)); + api, nullptr, wrapper_data_file_name.c_str(), nullptr, data)); EXPECT_EQ(std::string(data.begin(), data.end()), payload); const std::filesystem::path fallback_data_path = test_dir / "fallback_context_data.bin"; const std::string fallback_data_file_name = ep_context_data_utils::PathToUtf8String(fallback_data_path); ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( - api, FakeEpContextConfigGetWriteFunc, nullptr, "logical_context_data.bin", fallback_data_file_name.c_str(), - nullptr, payload.data(), payload.size())); + api, nullptr, "logical_context_data.bin", fallback_data_file_name.c_str(), nullptr, payload.data(), + payload.size())); data.clear(); ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataFromFile(api, fallback_data_file_name.c_str(), nullptr, @@ -749,11 +750,11 @@ TEST(OrtEpLibrary, EpContextDataUtils_FileFallbackReadsAndWrites) { const std::filesystem::path empty_data_path = test_dir / "empty_context_data.bin"; const std::string empty_data_file_name = ep_context_data_utils::PathToUtf8String(empty_data_path); ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( - api, FakeEpContextConfigGetWriteFunc, nullptr, empty_data_file_name.c_str(), nullptr, nullptr, 0)); + api, nullptr, empty_data_file_name.c_str(), nullptr, nullptr, 0)); data.assign({'s', 't', 'a', 'l', 'e'}); ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataWithFileFallback( - api, FakeEpContextConfigGetReadFunc, nullptr, empty_data_file_name.c_str(), nullptr, data)); + api, nullptr, empty_data_file_name.c_str(), nullptr, data)); EXPECT_TRUE(data.empty()); const std::filesystem::path missing_data_path = test_dir / "missing_context_data.bin"; @@ -1009,6 +1010,41 @@ TEST(OrtEpLibrary, PluginEp_GenEpContextModel_ExternalDataUsesWriteCallback) { EXPECT_EQ(std::string(callback_state.payload.begin(), callback_state.payload.end()), "binary_data"); } +TEST(OrtEpLibrary, PluginEp_GenEpContextModel_ExternalDataBufferOutputRequiresWriteCallback) { + RegisteredEpDeviceUniquePtr example_ep; + ASSERT_NO_FATAL_FAILURE(Utils::RegisterAndGetExampleEp(*ort_env, Utils::example_ep_info, example_ep)); + Ort::ConstEpDevice plugin_ep_device(example_ep.get()); + + const ORTCHAR_T* input_model_file = ORT_TSTR("testdata/mul_1.onnx"); + + Ort::SessionOptions session_options; + std::unordered_map ep_options; + session_options.AppendExecutionProvider_V2(*ort_env, {plugin_ep_device}, ep_options); + + Ort::AllocatorWithDefaultOptions allocator; + void* output_model_buffer = nullptr; + size_t output_model_buffer_size = 0; + auto release_output_model_buffer = gsl::finally([&]() { + if (output_model_buffer != nullptr) { + allocator.Free(output_model_buffer); + } + }); + + Ort::ModelCompilationOptions compile_options(*ort_env, session_options); + compile_options.SetFlags(OrtCompileApiFlags_ERROR_IF_NO_NODES_COMPILED); + compile_options.SetInputModelPath(input_model_file); + compile_options.SetOutputModelBuffer(allocator, &output_model_buffer, &output_model_buffer_size); + compile_options.SetEpContextEmbedMode(false); + + Ort::Status status = Ort::CompileModel(*ort_env, compile_options); + ASSERT_FALSE(status.IsOK()); + EXPECT_THAT(status.GetErrorMessage(), ::testing::HasSubstr( + "External EPContext data requires an output model path or an " + "OrtWriteNamedBufferFunc callback")); + EXPECT_EQ(output_model_buffer, nullptr); + EXPECT_EQ(output_model_buffer_size, 0U); +} + TEST(OrtEpLibrary, PluginEp_LoadEpContextModel_ExternalDataUsesReadCallback) { RegisteredEpDeviceUniquePtr example_ep; ASSERT_NO_FATAL_FAILURE(Utils::RegisterAndGetExampleEp(*ort_env, Utils::example_ep_info, example_ep)); From 9e303934acee9d1f8fc6e8c5e88ebca2ea8b16d0 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Thu, 11 Jun 2026 20:49:08 -0700 Subject: [PATCH 21/48] Clarify EPContext external-data error message and callback validation Name all three remedies (output model path, ep.context_file_path, or OrtWriteNamedBufferFunc callback) in the ModelCompilationOptions::Check() error and keep the matching test assertion in sync. Document why the write callback path forwards file_name unmodified while only the file fallback validates the logical name. --- onnxruntime/core/session/model_compilation_options.cc | 4 ++-- onnxruntime/test/autoep/library/ep_context_data_utils.h | 2 ++ onnxruntime/test/autoep/test_execution.cc | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/onnxruntime/core/session/model_compilation_options.cc b/onnxruntime/core/session/model_compilation_options.cc index 294afadc42786..ffc00992d365b 100644 --- a/onnxruntime/core/session/model_compilation_options.cc +++ b/onnxruntime/core/session/model_compilation_options.cc @@ -340,8 +340,8 @@ Status ModelCompilationOptions::Check() const { if (!ep_context_gen_options.embed_ep_context_in_model && !output_model_is_file && !has_ep_context_file_path && !has_ep_context_data_write_func) { return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, - "External EPContext data requires an output model path or an " - "OrtWriteNamedBufferFunc callback"); + "External EPContext data requires an output model path, an EPContext file path " + "(\"ep.context_file_path\"), or an OrtWriteNamedBufferFunc callback"); } const epctx::BufferHolder* output_buffer_ptr = ep_context_gen_options.TryGetOutputModelBuffer(); diff --git a/onnxruntime/test/autoep/library/ep_context_data_utils.h b/onnxruntime/test/autoep/library/ep_context_data_utils.h index a9dc225f832ae..b68d44e0610cd 100644 --- a/onnxruntime/test/autoep/library/ep_context_data_utils.h +++ b/onnxruntime/test/autoep/library/ep_context_data_utils.h @@ -332,6 +332,8 @@ inline OrtStatus* WriteEpContextDataWithFileFallback( RETURN_IF_ERROR(get_write_func(ep_context_config, &write_func, &write_state)); } + // The app-supplied write callback owns its own logical namespace, so file_name is passed through unmodified. + // Only the file-fallback path below maps a name onto the filesystem, so it validates the logical name there. if (write_func != nullptr) { return write_func(write_state, file_name, buffer, buffer_size); } diff --git a/onnxruntime/test/autoep/test_execution.cc b/onnxruntime/test/autoep/test_execution.cc index a80a8888d1c55..5a6e1c8bbf0b4 100644 --- a/onnxruntime/test/autoep/test_execution.cc +++ b/onnxruntime/test/autoep/test_execution.cc @@ -1038,9 +1038,9 @@ TEST(OrtEpLibrary, PluginEp_GenEpContextModel_ExternalDataBufferOutputRequiresWr Ort::Status status = Ort::CompileModel(*ort_env, compile_options); ASSERT_FALSE(status.IsOK()); - EXPECT_THAT(status.GetErrorMessage(), ::testing::HasSubstr( - "External EPContext data requires an output model path or an " - "OrtWriteNamedBufferFunc callback")); + EXPECT_THAT(status.GetErrorMessage(), + ::testing::HasSubstr("External EPContext data requires an output model path, an EPContext file path " + "(\"ep.context_file_path\"), or an OrtWriteNamedBufferFunc callback")); EXPECT_EQ(output_model_buffer, nullptr); EXPECT_EQ(output_model_buffer_size, 0U); } From 9c58baf6aa30fb0de61be4377010ccaab79ca9a7 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Thu, 11 Jun 2026 23:21:33 -0700 Subject: [PATCH 22/48] Remove premature EPContext external-data validation ModelCompilationOptions::Check() cannot know whether compilation will emit external EPContext data, and the default non-embed mode caused valid delegate/buffer compile flows to fail before compilation. Remove the false-positive validation and the PR-added test that asserted it. --- .../core/session/model_compilation_options.cc | 11 ------ onnxruntime/test/autoep/test_execution.cc | 35 ------------------- 2 files changed, 46 deletions(-) diff --git a/onnxruntime/core/session/model_compilation_options.cc b/onnxruntime/core/session/model_compilation_options.cc index ffc00992d365b..32dd4a4a77e9c 100644 --- a/onnxruntime/core/session/model_compilation_options.cc +++ b/onnxruntime/core/session/model_compilation_options.cc @@ -333,17 +333,6 @@ Status ModelCompilationOptions::Check() const { "of the output model (e.g., file, buffer, or stream) is not specified."); } - const bool output_model_is_file = ep_context_gen_options.TryGetOutputModelPath() != nullptr; - const bool has_ep_context_data_write_func = ep_context_gen_options.TryGetEpContextDataWriteFunc() != nullptr; - const bool has_ep_context_file_path = - !config_options.GetConfigOrDefault(kOrtSessionOptionEpContextFilePath, "").empty(); - if (!ep_context_gen_options.embed_ep_context_in_model && !output_model_is_file && !has_ep_context_file_path && - !has_ep_context_data_write_func) { - return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, - "External EPContext data requires an output model path, an EPContext file path " - "(\"ep.context_file_path\"), or an OrtWriteNamedBufferFunc callback"); - } - const epctx::BufferHolder* output_buffer_ptr = ep_context_gen_options.TryGetOutputModelBuffer(); if (output_buffer_ptr != nullptr && output_buffer_ptr->buffer_ptr == nullptr) { diff --git a/onnxruntime/test/autoep/test_execution.cc b/onnxruntime/test/autoep/test_execution.cc index 5a6e1c8bbf0b4..27a00148a0d21 100644 --- a/onnxruntime/test/autoep/test_execution.cc +++ b/onnxruntime/test/autoep/test_execution.cc @@ -1010,41 +1010,6 @@ TEST(OrtEpLibrary, PluginEp_GenEpContextModel_ExternalDataUsesWriteCallback) { EXPECT_EQ(std::string(callback_state.payload.begin(), callback_state.payload.end()), "binary_data"); } -TEST(OrtEpLibrary, PluginEp_GenEpContextModel_ExternalDataBufferOutputRequiresWriteCallback) { - RegisteredEpDeviceUniquePtr example_ep; - ASSERT_NO_FATAL_FAILURE(Utils::RegisterAndGetExampleEp(*ort_env, Utils::example_ep_info, example_ep)); - Ort::ConstEpDevice plugin_ep_device(example_ep.get()); - - const ORTCHAR_T* input_model_file = ORT_TSTR("testdata/mul_1.onnx"); - - Ort::SessionOptions session_options; - std::unordered_map ep_options; - session_options.AppendExecutionProvider_V2(*ort_env, {plugin_ep_device}, ep_options); - - Ort::AllocatorWithDefaultOptions allocator; - void* output_model_buffer = nullptr; - size_t output_model_buffer_size = 0; - auto release_output_model_buffer = gsl::finally([&]() { - if (output_model_buffer != nullptr) { - allocator.Free(output_model_buffer); - } - }); - - Ort::ModelCompilationOptions compile_options(*ort_env, session_options); - compile_options.SetFlags(OrtCompileApiFlags_ERROR_IF_NO_NODES_COMPILED); - compile_options.SetInputModelPath(input_model_file); - compile_options.SetOutputModelBuffer(allocator, &output_model_buffer, &output_model_buffer_size); - compile_options.SetEpContextEmbedMode(false); - - Ort::Status status = Ort::CompileModel(*ort_env, compile_options); - ASSERT_FALSE(status.IsOK()); - EXPECT_THAT(status.GetErrorMessage(), - ::testing::HasSubstr("External EPContext data requires an output model path, an EPContext file path " - "(\"ep.context_file_path\"), or an OrtWriteNamedBufferFunc callback")); - EXPECT_EQ(output_model_buffer, nullptr); - EXPECT_EQ(output_model_buffer_size, 0U); -} - TEST(OrtEpLibrary, PluginEp_LoadEpContextModel_ExternalDataUsesReadCallback) { RegisteredEpDeviceUniquePtr example_ep; ASSERT_NO_FATAL_FAILURE(Utils::RegisterAndGetExampleEp(*ort_env, Utils::example_ep_info, example_ep)); From c50584cf196068dc1498c1b9ed7df2cfae9c0ba4 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Tue, 16 Jun 2026 14:45:29 -0700 Subject: [PATCH 23/48] Address EPContext review feedback: docs, path symmetry, lifetime, style - Guard with NOMINMAX/WIN32_LEAN_AND_MEAN in ep_context_data_utils.h. - Reject absolute paths in ResolveEpContextDataOutputPath when graph != nullptr, matching the read-side resolver so production EPs cannot route untrusted model-derived names to absolute locations. - Document the EP-implementer helper entry points and the read/write fallback overloads. - Fix raw OrtEpContextConfig leak in CreateEpImpl if ExampleEp construction throws (scoped release guard). - Use ORT_API_RETURN_IF before the cast in SetEpContextDataWriteFunc and note the null-write_func rejection vs read clear-semantics in the API doc. - Comment why session_options.h needs the experimental header. - Rename test helper to ExpectFailureOrtStatus (document OrtStatus ownership) and release_config_fn -> release_config_func. --- .../onnxruntime_experimental_c_api.inc | 6 +- onnxruntime/core/framework/session_options.h | 2 + .../core/session/experimental_c_api.cc | 12 +--- .../autoep/library/ep_context_data_utils.h | 38 +++++++++- .../library/example_plugin_ep/ep_factory.cc | 17 ++++- .../test/framework/ep_plugin_provider_test.cc | 70 ++++++++++--------- 6 files changed, 98 insertions(+), 47 deletions(-) diff --git a/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc b/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc index cf17b1509a189..cf5b0d7c7b844 100644 --- a/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc +++ b/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc @@ -319,8 +319,12 @@ ORT_EXPERIMENTAL_API(28, OrtStatusPtr, OrtApi_SessionOptions_SetEpContextDataRea * operation that may call the callback. If the same state may be used by multiple EPs or threads, the application is * responsible for synchronization. * + * Unlike OrtApi_SessionOptions_SetEpContextDataReadFunc, which accepts a NULL callback to clear a previously set one, + * write_func must not be NULL here: compilation options are configured once per compile, so there is no prior + * callback to clear and a NULL write_func is rejected with ORT_INVALID_ARGUMENT. + * * \param[in] model_compile_options The OrtModelCompilationOptions instance. - * \param[in] write_func The OrtWriteNamedBufferFunc callback used to write EPContext bytes. + * \param[in] write_func The OrtWriteNamedBufferFunc callback used to write EPContext bytes. Must not be NULL. * \param[in] state Opaque state passed to write_func. Can be NULL. * * \snippet{doc} snippets.dox OrtStatus Return Value diff --git a/onnxruntime/core/framework/session_options.h b/onnxruntime/core/framework/session_options.h index 6f7755207e492..ddd21074afe8a 100644 --- a/onnxruntime/core/framework/session_options.h +++ b/onnxruntime/core/framework/session_options.h @@ -16,6 +16,8 @@ #include "core/framework/ep_context_options.h" #include "core/framework/ort_value.h" #include "core/session/onnxruntime_c_api.h" +// Needed for OrtReadNamedBufferFunc, the type of the EPContext data read callback stored in this struct. This include +// can be removed once the experimental EPContext data callback APIs are promoted to the stable C API. #include "core/session/onnxruntime_experimental_c_api.h" #include "core/optimizer/graph_transformer_level.h" #include "core/util/thread_utils.h" diff --git a/onnxruntime/core/session/experimental_c_api.cc b/onnxruntime/core/session/experimental_c_api.cc index ad522e0326e1e..950bb3a022a6b 100644 --- a/onnxruntime/core/session/experimental_c_api.cc +++ b/onnxruntime/core/session/experimental_c_api.cc @@ -75,16 +75,10 @@ ORT_API_STATUS_IMPL(OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteF _In_ OrtWriteNamedBufferFunc write_func, _In_opt_ void* state) { API_IMPL_BEGIN #if !defined(ORT_MINIMAL_BUILD) - auto model_compile_options = reinterpret_cast(ort_model_compile_options); - - if (model_compile_options == nullptr) { - return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, "OrtModelCompilationOptions is NULL"); - } - - if (write_func == nullptr) { - return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, "OrtWriteNamedBufferFunc function is NULL"); - } + ORT_API_RETURN_IF(ort_model_compile_options == nullptr, ORT_INVALID_ARGUMENT, "OrtModelCompilationOptions is NULL"); + ORT_API_RETURN_IF(write_func == nullptr, ORT_INVALID_ARGUMENT, "OrtWriteNamedBufferFunc function is NULL"); + auto model_compile_options = reinterpret_cast(ort_model_compile_options); model_compile_options->SetEpContextDataWriteFunc(write_func, state); return nullptr; #else diff --git a/onnxruntime/test/autoep/library/ep_context_data_utils.h b/onnxruntime/test/autoep/library/ep_context_data_utils.h index b68d44e0610cd..35aecf1d4b1a2 100644 --- a/onnxruntime/test/autoep/library/ep_context_data_utils.h +++ b/onnxruntime/test/autoep/library/ep_context_data_utils.h @@ -13,13 +13,29 @@ #include #ifdef _WIN32 +// Define NOMINMAX (and WIN32_LEAN_AND_MEAN) before so the min/max macros it would otherwise pull in do +// not clobber std::numeric_limits<...>::max() and std::min/std::max used in this header. +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif #include #endif #include "plugin_ep_utils.h" #include "onnxruntime_experimental_c_api.h" -// Sample-only EPContext data helpers. These are intentionally outside the ORT C and EP ABI. +// Sample-only EPContext data helpers shared by the example plugin EP and its tests. These are intentionally outside +// the ORT C and EP ABI and are provided as a reference for EP authors that need to handle external (non-embedded) +// EPContext binary data. +// +// The intended entry points for EP implementers are the ReadEpContextDataWithFileFallback / +// WriteEpContextDataWithFileFallback overloads: they prefer an application-supplied OrtReadNamedBufferFunc / +// OrtWriteNamedBufferFunc (carried by OrtEpContextConfig) and fall back to file I/O when no callback is configured. +// The other functions are lower-level building blocks. Production EPs should additionally apply their own sandboxing, +// size limits, and path policies; see the per-function notes on how untrusted, model-derived names are treated. namespace ep_context_data_utils { #ifdef _WIN32 @@ -169,10 +185,17 @@ inline OrtStatus* ResolveEpContextDataOutputPath(const OrtApi& api, const char* return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not contain path traversal"); } - if (graph == nullptr || data_path.is_absolute()) { + // Trusted direct callers (graph == nullptr) may supply an absolute physical path. When a graph is present, the + // name may be model-derived (untrusted), so reject absolute paths for symmetry with the read-side + // ResolveEpContextDataPath instead of writing to an attacker-chosen absolute location. + if (graph == nullptr) { return nullptr; } + if (data_path.is_absolute()) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute"); + } + const ORTCHAR_T* model_path = nullptr; RETURN_IF_ERROR(api.Graph_GetModelPath(graph, &model_path)); if (model_path == nullptr || model_path[0] == 0) { @@ -296,6 +319,11 @@ inline OrtStatus* ReadEpContextDataWithFileFallback( return nullptr; } +// Reads EPContext binary data named `file_name`. If the session configured an OrtReadNamedBufferFunc (carried by +// `ep_context_config`), it is used; otherwise the data is read from a file. When `graph` is non-null it is the +// EPContext model graph: untrusted absolute/traversal names are rejected and relative names are resolved against the +// model directory. Pass `graph == nullptr` only for trusted callers supplying a physical path. `data` is cleared +// first and receives the bytes on success. inline OrtStatus* ReadEpContextDataWithFileFallback( const OrtApi& api, const OrtEpContextConfig* ep_context_config, @@ -350,6 +378,11 @@ inline OrtStatus* WriteEpContextDataWithFileFallback( return WriteEpContextDataToResolvedFile(api, data_path, buffer, buffer_size); } +// Writes EPContext binary data. If the compilation configured an OrtWriteNamedBufferFunc (carried by +// `ep_context_config`), it is used and `file_name` is passed through unmodified as the logical name. Otherwise the +// data is written to a file at `fallback_file_name`, which is resolved against the model directory when `graph` is +// non-null (and rejected if absolute in that case). `graph == nullptr` denotes a trusted caller that may supply an +// absolute physical path. `buffer` may be null only when `buffer_size` is 0. inline OrtStatus* WriteEpContextDataWithFileFallback( const OrtApi& api, const OrtEpContextConfig* ep_context_config, @@ -372,6 +405,7 @@ inline OrtStatus* WriteEpContextDataWithFileFallback( buffer_size); } +// Convenience overload that uses `file_name` as both the logical callback name and the file-fallback path. inline OrtStatus* WriteEpContextDataWithFileFallback( const OrtApi& api, const OrtEpContextConfig* ep_context_config, diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc index 299e88c251502..989f8e8e13f2d 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc @@ -246,8 +246,21 @@ OrtStatus* ORT_API_CALL ExampleEpFactory::CreateEpImpl(OrtEpFactory* this_ptr, } RETURN_IF_ERROR(get_ep_context_config(session_options, &ep_context_config)); - auto dummy_ep = std::make_unique(*factory, factory->ep_name_, config, *logger, - ep_context_config); + // Own the config locally until ExampleEp takes ownership below, so it is released if ExampleEp construction throws. + // (A future Ort::Experimental::EpContextConfig RAII wrapper would replace this manual guard.) + auto release_ep_context_config = [factory](OrtEpContextConfig* config) { + if (config == nullptr) { + return; + } + if (auto* release_fn = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&factory->ort_api)) { + release_fn(config); + } + }; + std::unique_ptr ep_context_config_guard( + ep_context_config, release_ep_context_config); + + auto dummy_ep = std::make_unique(*factory, factory->ep_name_, config, *logger, ep_context_config); + ep_context_config_guard.release(); // ExampleEp now owns the config and releases it in its destructor. *ep = dummy_ep.release(); return nullptr; diff --git a/onnxruntime/test/framework/ep_plugin_provider_test.cc b/onnxruntime/test/framework/ep_plugin_provider_test.cc index aa16c7e83ae34..c41ad447c6d39 100644 --- a/onnxruntime/test/framework/ep_plugin_provider_test.cc +++ b/onnxruntime/test/framework/ep_plugin_provider_test.cc @@ -62,7 +62,10 @@ static void CheckFileIsEmpty(const PathString& filename) { EXPECT_TRUE(content.empty()); } -static void ExpectOrtStatus(OrtStatus* status_ptr, OrtErrorCode expected_code, const char* expected_message) { +// Asserts that `status_ptr` is a failure status with the expected error code and message substring. +// Takes ownership of `status_ptr` (wraps it in an Ort::Status that releases it on scope exit), so callers should pass +// the OrtStatus* returned directly by the API under test without releasing it themselves. +static void ExpectFailureOrtStatus(OrtStatus* status_ptr, OrtErrorCode expected_code, const char* expected_message) { Ort::Status status{status_ptr}; ASSERT_FALSE(status.IsOK()); EXPECT_EQ(status.GetErrorCode(), expected_code); @@ -1781,11 +1784,11 @@ TEST(PluginExecutionProviderTest, EpContextDataReadFuncIsReturnedByEpApi) { auto set_read_func = Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_Fn(&ort_api); auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&ort_api); - auto release_config_fn = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); + auto release_config_func = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); auto get_read_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&ort_api); ASSERT_NE(set_read_func, nullptr); ASSERT_NE(get_config, nullptr); - ASSERT_NE(release_config_fn, nullptr); + ASSERT_NE(release_config_func, nullptr); ASSERT_NE(get_read_func, nullptr); EpContextReadCallbackState callback_state{ @@ -1797,7 +1800,7 @@ TEST(PluginExecutionProviderTest, EpContextDataReadFuncIsReturnedByEpApi) { OrtEpContextConfig* ep_context_config = nullptr; ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); - auto release_config = gsl::finally([&]() { release_config_fn(ep_context_config); }); + auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); OrtReadNamedBufferFunc read_func = nullptr; void* callback_state_out = nullptr; @@ -1826,43 +1829,44 @@ TEST(PluginExecutionProviderTest, EpContextDataApiRejectsInvalidArguments) { const auto& ort_api = Ort::GetApi(); auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&ort_api); - auto release_config_fn = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); + auto release_config_func = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); auto get_read_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&ort_api); auto get_write_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); auto set_read_func = Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_Fn(&ort_api); ASSERT_NE(get_config, nullptr); - ASSERT_NE(release_config_fn, nullptr); + ASSERT_NE(release_config_func, nullptr); ASSERT_NE(get_read_func, nullptr); ASSERT_NE(get_write_func, nullptr); ASSERT_NE(set_read_func, nullptr); Ort::SessionOptions session_options; OrtEpContextConfig* ep_context_config = nullptr; - ExpectOrtStatus(get_config(nullptr, &ep_context_config), ORT_INVALID_ARGUMENT, "OrtSessionOptions is NULL"); - ExpectOrtStatus(get_config(session_options, nullptr), ORT_INVALID_ARGUMENT, "Output OrtEpContextConfig is NULL"); + ExpectFailureOrtStatus(get_config(nullptr, &ep_context_config), ORT_INVALID_ARGUMENT, "OrtSessionOptions is NULL"); + ExpectFailureOrtStatus(get_config(session_options, nullptr), ORT_INVALID_ARGUMENT, + "Output OrtEpContextConfig is NULL"); - ExpectOrtStatus(set_read_func(nullptr, EpContextReadCallback, nullptr), + ExpectFailureOrtStatus(set_read_func(nullptr, EpContextReadCallback, nullptr), ORT_INVALID_ARGUMENT, "'options' parameter must not be NULL"); // A null read_func is allowed: it clears any previously set callback (covered by // EpContextDataReadFuncCanBeCleared), so it is not rejected here. ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); - auto release_config = gsl::finally([&]() { release_config_fn(ep_context_config); }); + auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); OrtReadNamedBufferFunc read_func = nullptr; OrtWriteNamedBufferFunc write_func = nullptr; void* state = nullptr; - ExpectOrtStatus(get_read_func(nullptr, &read_func, &state), + ExpectFailureOrtStatus(get_read_func(nullptr, &read_func, &state), ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); - ExpectOrtStatus(get_read_func(ep_context_config, nullptr, &state), + ExpectFailureOrtStatus(get_read_func(ep_context_config, nullptr, &state), ORT_INVALID_ARGUMENT, "Output read_func is NULL"); - ExpectOrtStatus(get_read_func(ep_context_config, &read_func, nullptr), + ExpectFailureOrtStatus(get_read_func(ep_context_config, &read_func, nullptr), ORT_INVALID_ARGUMENT, "Output state is NULL"); - ExpectOrtStatus(get_write_func(nullptr, &write_func, &state), + ExpectFailureOrtStatus(get_write_func(nullptr, &write_func, &state), ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); - ExpectOrtStatus(get_write_func(ep_context_config, nullptr, &state), + ExpectFailureOrtStatus(get_write_func(ep_context_config, nullptr, &state), ORT_INVALID_ARGUMENT, "Output write_func is NULL"); - ExpectOrtStatus(get_write_func(ep_context_config, &write_func, nullptr), + ExpectFailureOrtStatus(get_write_func(ep_context_config, &write_func, nullptr), ORT_INVALID_ARGUMENT, "Output state is NULL"); #if !defined(ORT_MINIMAL_BUILD) @@ -1871,9 +1875,9 @@ TEST(PluginExecutionProviderTest, EpContextDataApiRejectsInvalidArguments) { auto set_write_func = Ort::Experimental::Get_OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); ASSERT_NE(set_write_func, nullptr); - ExpectOrtStatus(set_write_func(nullptr, EpContextWriteCallback, nullptr), + ExpectFailureOrtStatus(set_write_func(nullptr, EpContextWriteCallback, nullptr), ORT_INVALID_ARGUMENT, "OrtModelCompilationOptions is NULL"); - ExpectOrtStatus(set_write_func(compilation_options, nullptr, nullptr), + ExpectFailureOrtStatus(set_write_func(compilation_options, nullptr, nullptr), ORT_INVALID_ARGUMENT, "OrtWriteNamedBufferFunc function is NULL"); #endif // !defined(ORT_MINIMAL_BUILD) } @@ -1883,17 +1887,17 @@ TEST(PluginExecutionProviderTest, EpContextDataAccessorsReturnNullWhenCallbacksU Ort::SessionOptions session_options; auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&ort_api); - auto release_config_fn = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); + auto release_config_func = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); auto get_read_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&ort_api); auto get_write_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); ASSERT_NE(get_config, nullptr); - ASSERT_NE(release_config_fn, nullptr); + ASSERT_NE(release_config_func, nullptr); ASSERT_NE(get_read_func, nullptr); ASSERT_NE(get_write_func, nullptr); OrtEpContextConfig* ep_context_config = nullptr; ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); - auto release_config = gsl::finally([&]() { release_config_fn(ep_context_config); }); + auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); OrtReadNamedBufferFunc read_func = EpContextReadCallback; OrtWriteNamedBufferFunc write_func = EpContextWriteCallback; @@ -1914,12 +1918,12 @@ TEST(PluginExecutionProviderTest, EpContextConfigReturnsConfiguredCallbacks) { auto set_read_func = Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_Fn(&ort_api); auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&ort_api); - auto release_config_fn = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); + auto release_config_func = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); auto get_read_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&ort_api); auto get_write_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); ASSERT_NE(set_read_func, nullptr); ASSERT_NE(get_config, nullptr); - ASSERT_NE(release_config_fn, nullptr); + ASSERT_NE(release_config_func, nullptr); ASSERT_NE(get_read_func, nullptr); ASSERT_NE(get_write_func, nullptr); @@ -1928,7 +1932,7 @@ TEST(PluginExecutionProviderTest, EpContextConfigReturnsConfiguredCallbacks) { OrtEpContextConfig* ep_context_config = nullptr; ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); - auto release_config = gsl::finally([&]() { release_config_fn(ep_context_config); }); + auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); OrtReadNamedBufferFunc read_func = nullptr; void* read_state = nullptr; @@ -1949,11 +1953,11 @@ TEST(PluginExecutionProviderTest, EpContextDataReadFuncCanBeCleared) { auto set_read_func = Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_Fn(&ort_api); auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&ort_api); - auto release_config_fn = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); + auto release_config_func = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); auto get_read_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&ort_api); ASSERT_NE(set_read_func, nullptr); ASSERT_NE(get_config, nullptr); - ASSERT_NE(release_config_fn, nullptr); + ASSERT_NE(release_config_func, nullptr); ASSERT_NE(get_read_func, nullptr); EpContextReadCallbackState callback_state{}; @@ -1965,7 +1969,7 @@ TEST(PluginExecutionProviderTest, EpContextDataReadFuncCanBeCleared) { OrtEpContextConfig* ep_context_config = nullptr; ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); - auto release_config = gsl::finally([&]() { release_config_fn(ep_context_config); }); + auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); OrtReadNamedBufferFunc read_func = EpContextReadCallback; void* read_state = reinterpret_cast(0x1); @@ -1984,11 +1988,11 @@ TEST(PluginExecutionProviderTest, EpContextDataWriteFuncIsReturnedByEpApi) { auto set_write_func = Ort::Experimental::Get_OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&ort_api); - auto release_config_fn = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); + auto release_config_func = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); auto get_write_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); ASSERT_NE(set_write_func, nullptr); ASSERT_NE(get_config, nullptr); - ASSERT_NE(release_config_fn, nullptr); + ASSERT_NE(release_config_func, nullptr); ASSERT_NE(get_write_func, nullptr); EpContextWriteCallbackState callback_state{}; @@ -1999,7 +2003,7 @@ TEST(PluginExecutionProviderTest, EpContextDataWriteFuncIsReturnedByEpApi) { OrtEpContextConfig* ep_context_config = nullptr; ASSERT_ORTSTATUS_OK(get_config(&internal_options->GetSessionOptions(), &ep_context_config)); - auto release_config = gsl::finally([&]() { release_config_fn(ep_context_config); }); + auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); OrtWriteNamedBufferFunc write_func = nullptr; void* callback_state_out = nullptr; @@ -2022,11 +2026,11 @@ TEST(PluginExecutionProviderTest, EpContextDataReturnedReadFuncAllowsEmptyPayloa auto set_read_func = Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_Fn(&ort_api); auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&ort_api); - auto release_config_fn = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); + auto release_config_func = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); auto get_read_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&ort_api); ASSERT_NE(set_read_func, nullptr); ASSERT_NE(get_config, nullptr); - ASSERT_NE(release_config_fn, nullptr); + ASSERT_NE(release_config_func, nullptr); ASSERT_NE(get_read_func, nullptr); EpContextReadCallbackState callback_state{}; @@ -2034,7 +2038,7 @@ TEST(PluginExecutionProviderTest, EpContextDataReturnedReadFuncAllowsEmptyPayloa OrtEpContextConfig* ep_context_config = nullptr; ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); - auto release_config = gsl::finally([&]() { release_config_fn(ep_context_config); }); + auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); OrtReadNamedBufferFunc read_func = nullptr; void* read_state = nullptr; From 24646030a63c93cf0293edb10e37e2e40b9a1094 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Thu, 18 Jun 2026 12:19:59 -0700 Subject: [PATCH 24/48] Apply clang-format alignment to ExpectFailureOrtStatus calls --- .../test/framework/ep_plugin_provider_test.cc | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/onnxruntime/test/framework/ep_plugin_provider_test.cc b/onnxruntime/test/framework/ep_plugin_provider_test.cc index c41ad447c6d39..5d2862d90e98c 100644 --- a/onnxruntime/test/framework/ep_plugin_provider_test.cc +++ b/onnxruntime/test/framework/ep_plugin_provider_test.cc @@ -1846,7 +1846,7 @@ TEST(PluginExecutionProviderTest, EpContextDataApiRejectsInvalidArguments) { "Output OrtEpContextConfig is NULL"); ExpectFailureOrtStatus(set_read_func(nullptr, EpContextReadCallback, nullptr), - ORT_INVALID_ARGUMENT, "'options' parameter must not be NULL"); + ORT_INVALID_ARGUMENT, "'options' parameter must not be NULL"); // A null read_func is allowed: it clears any previously set callback (covered by // EpContextDataReadFuncCanBeCleared), so it is not rejected here. @@ -1857,17 +1857,17 @@ TEST(PluginExecutionProviderTest, EpContextDataApiRejectsInvalidArguments) { OrtWriteNamedBufferFunc write_func = nullptr; void* state = nullptr; ExpectFailureOrtStatus(get_read_func(nullptr, &read_func, &state), - ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); + ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); ExpectFailureOrtStatus(get_read_func(ep_context_config, nullptr, &state), - ORT_INVALID_ARGUMENT, "Output read_func is NULL"); + ORT_INVALID_ARGUMENT, "Output read_func is NULL"); ExpectFailureOrtStatus(get_read_func(ep_context_config, &read_func, nullptr), - ORT_INVALID_ARGUMENT, "Output state is NULL"); + ORT_INVALID_ARGUMENT, "Output state is NULL"); ExpectFailureOrtStatus(get_write_func(nullptr, &write_func, &state), - ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); + ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); ExpectFailureOrtStatus(get_write_func(ep_context_config, nullptr, &state), - ORT_INVALID_ARGUMENT, "Output write_func is NULL"); + ORT_INVALID_ARGUMENT, "Output write_func is NULL"); ExpectFailureOrtStatus(get_write_func(ep_context_config, &write_func, nullptr), - ORT_INVALID_ARGUMENT, "Output state is NULL"); + ORT_INVALID_ARGUMENT, "Output state is NULL"); #if !defined(ORT_MINIMAL_BUILD) Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "EpContextDataApiRejectsInvalidArguments"}; @@ -1876,9 +1876,9 @@ TEST(PluginExecutionProviderTest, EpContextDataApiRejectsInvalidArguments) { Ort::Experimental::Get_OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); ASSERT_NE(set_write_func, nullptr); ExpectFailureOrtStatus(set_write_func(nullptr, EpContextWriteCallback, nullptr), - ORT_INVALID_ARGUMENT, "OrtModelCompilationOptions is NULL"); + ORT_INVALID_ARGUMENT, "OrtModelCompilationOptions is NULL"); ExpectFailureOrtStatus(set_write_func(compilation_options, nullptr, nullptr), - ORT_INVALID_ARGUMENT, "OrtWriteNamedBufferFunc function is NULL"); + ORT_INVALID_ARGUMENT, "OrtWriteNamedBufferFunc function is NULL"); #endif // !defined(ORT_MINIMAL_BUILD) } From d64d7082185ed17cf2c119892ab40043efaefce3 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Thu, 18 Jun 2026 12:40:48 -0700 Subject: [PATCH 25/48] Add Ort::Experimental::EpContextConfig RAII wrapper and use it in the example EP Adds a move-only RAII owner for OrtEpContextConfig in the experimental C++ section. ExampleEp now holds the wrapper (defaulted destructor) and ExampleEpFactory::CreateEpImpl transfers ownership into it, removing the manual release guard and the hand-written destructor cleanup. --- .../session/onnxruntime_experimental_c_api.h | 64 +++++++++++++++++++ .../autoep/library/example_plugin_ep/ep.cc | 18 ++---- .../autoep/library/example_plugin_ep/ep.h | 6 +- .../library/example_plugin_ep/ep_factory.cc | 20 ++---- 4 files changed, 77 insertions(+), 31 deletions(-) diff --git a/include/onnxruntime/core/session/onnxruntime_experimental_c_api.h b/include/onnxruntime/core/session/onnxruntime_experimental_c_api.h index 3f74832bb89cd..f4f7dd0d8fa09 100644 --- a/include/onnxruntime/core/session/onnxruntime_experimental_c_api.h +++ b/include/onnxruntime/core/session/onnxruntime_experimental_c_api.h @@ -160,6 +160,70 @@ namespace Experimental { #undef ORT_EXPERIMENTAL_API +// +// C++ wrapper types for the experimental APIs +// + +// Move-only RAII owner for an OrtEpContextConfig handle obtained from OrtEpApi_SessionOptions_GetEpContextConfig. +// Releases the handle via OrtEpApi_ReleaseEpContextConfig when destroyed. +// +// Note: intentionally lightweight and exception-free so this header (which only depends on the C API) can be included +// by ORT-internal code without pulling in onnxruntime_cxx_api.h. +class EpContextConfig { + public: + EpContextConfig() noexcept = default; + + // Takes ownership of `config`. `api` is used to release it; both may be null (an empty wrapper). + EpContextConfig(const OrtApi* api, OrtEpContextConfig* config) noexcept : api_{api}, config_{config} {} + + EpContextConfig(EpContextConfig&& other) noexcept : api_{other.api_}, config_{other.config_} { + other.api_ = nullptr; + other.config_ = nullptr; + } + + EpContextConfig& operator=(EpContextConfig&& other) noexcept { + if (this != &other) { + reset(); + api_ = other.api_; + config_ = other.config_; + other.api_ = nullptr; + other.config_ = nullptr; + } + return *this; + } + + EpContextConfig(const EpContextConfig&) = delete; + EpContextConfig& operator=(const EpContextConfig&) = delete; + + ~EpContextConfig() { reset(); } + + OrtEpContextConfig* get() const noexcept { return config_; } + explicit operator bool() const noexcept { return config_ != nullptr; } + + // Relinquishes ownership of the handle without releasing it. + OrtEpContextConfig* release() noexcept { + OrtEpContextConfig* released = config_; + api_ = nullptr; + config_ = nullptr; + return released; + } + + // Releases any owned handle and resets to empty. + void reset() noexcept { + if (api_ != nullptr && config_ != nullptr) { + if (auto* release_fn = Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(api_)) { + release_fn(config_); + } + } + api_ = nullptr; + config_ = nullptr; + } + + private: + const OrtApi* api_ = nullptr; + OrtEpContextConfig* config_ = nullptr; +}; + } // namespace Experimental } // namespace Ort diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc index 332d716401daf..bc5e0d4e2661d 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc @@ -11,6 +11,7 @@ #include #include #include +#include #include #include "../ep_context_data_utils.h" @@ -169,14 +170,14 @@ struct EpContextNodeComputeInfo : NodeComputeInfoBase { }; ExampleEp::ExampleEp(ExampleEpFactory& factory, const std::string& name, const Config& config, const OrtLogger& logger, - OrtEpContextConfig* ep_context_config) + Ort::Experimental::EpContextConfig ep_context_config) : OrtEp{}, // explicitly call the struct ctor to ensure all optional values are default initialized ApiPtrs{static_cast(factory)}, factory_{factory}, name_{name}, config_{config}, logger_{logger}, - ep_context_config_{ep_context_config} { + ep_context_config_{std::move(ep_context_config)} { ort_version_supported = ORT_API_VERSION; // set to the ORT version we were compiled with. // Initialize the execution provider's function table @@ -196,15 +197,6 @@ ExampleEp::ExampleEp(ExampleEpFactory& factory, const std::string& name, const C ORT_FILE, __LINE__, __FUNCTION__)); } -ExampleEp::~ExampleEp() { - if (ep_context_config_ != nullptr) { - if (auto* release_ep_context_config = - Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api)) { - release_ep_context_config(ep_context_config_); - } - } -} - /*static*/ const char* ORT_API_CALL ExampleEp ::GetNameImpl(const OrtEp* this_ptr) noexcept { const auto* ep = static_cast(this_ptr); @@ -432,7 +424,7 @@ OrtStatus* ORT_API_CALL ExampleEp::CompileImpl(_In_ OrtEp* this_ptr, _In_ const std::vector ep_context_data; RETURN_IF_ERROR(ep_context_data_utils::ReadEpContextDataWithFileFallback( - ep->ort_api, ep->ep_context_config_, ep_cache_context.c_str(), ort_graphs[0], + ep->ort_api, ep->ep_context_config_.get(), ep_cache_context.c_str(), ort_graphs[0], ep_context_data)); } @@ -560,7 +552,7 @@ OrtStatus* ExampleEp::CreateEpContextNodes(const OrtGraph* graph, fallback_graph = nullptr; } RETURN_IF_ERROR(ep_context_data_utils::WriteEpContextDataWithFileFallback( - ort_api, ep_context_config_, ep_ctx.c_str(), fallback_ep_ctx.c_str(), fallback_graph, + ort_api, ep_context_config_.get(), ep_ctx.c_str(), fallback_ep_ctx.c_str(), fallback_graph, ep_context_data.data(), ep_context_data.size())); } attributes[0] = Ort::OpAttr("ep_cache_context", ep_ctx.data(), static_cast(ep_ctx.size()), diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep.h b/onnxruntime/test/autoep/library/example_plugin_ep/ep.h index fe0cf8712af40..22ae4bf990e14 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep.h +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep.h @@ -69,9 +69,9 @@ class ExampleEp : public OrtEp, public ApiPtrs { }; ExampleEp(ExampleEpFactory& factory, const std::string& name, const Config& config, const OrtLogger& logger, - OrtEpContextConfig* ep_context_config); + Ort::Experimental::EpContextConfig ep_context_config); - ~ExampleEp(); + ~ExampleEp() = default; std::unordered_map>& MulKernels() { return mul_kernels_; @@ -127,7 +127,7 @@ class ExampleEp : public OrtEp, public ApiPtrs { std::string name_; Config config_{}; const OrtLogger& logger_; - OrtEpContextConfig* ep_context_config_ = nullptr; + Ort::Experimental::EpContextConfig ep_context_config_; std::unordered_map> mul_kernels_; std::unordered_map> ep_context_kernels_; std::unordered_map float_initializers_; diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc index 989f8e8e13f2d..953961da3ef7e 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc @@ -246,21 +246,11 @@ OrtStatus* ORT_API_CALL ExampleEpFactory::CreateEpImpl(OrtEpFactory* this_ptr, } RETURN_IF_ERROR(get_ep_context_config(session_options, &ep_context_config)); - // Own the config locally until ExampleEp takes ownership below, so it is released if ExampleEp construction throws. - // (A future Ort::Experimental::EpContextConfig RAII wrapper would replace this manual guard.) - auto release_ep_context_config = [factory](OrtEpContextConfig* config) { - if (config == nullptr) { - return; - } - if (auto* release_fn = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&factory->ort_api)) { - release_fn(config); - } - }; - std::unique_ptr ep_context_config_guard( - ep_context_config, release_ep_context_config); - - auto dummy_ep = std::make_unique(*factory, factory->ep_name_, config, *logger, ep_context_config); - ep_context_config_guard.release(); // ExampleEp now owns the config and releases it in its destructor. + // ExampleEp takes ownership of the config via the RAII wrapper; if construction throws, the temporary wrapper + // releases the config. + auto dummy_ep = std::make_unique( + *factory, factory->ep_name_, config, *logger, + Ort::Experimental::EpContextConfig{&factory->ort_api, ep_context_config}); *ep = dummy_ep.release(); return nullptr; From 57c262e498334a27eb18fc75b78b8839b1b0767c Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Thu, 18 Jun 2026 16:16:47 -0700 Subject: [PATCH 26/48] Replace reinterpret_cast EPContext config fake with a real config token The low-level *WithFileFallback overloads only forward the OrtEpContextConfig to the injected getter, so the tests now pair a real (empty) config (owned by Ort::Experimental::EpContextConfig) with getters that read callbacks from a serially-set holder, removing the undefined-behavior cast between distinct types. --- onnxruntime/test/autoep/test_execution.cc | 48 ++++++++++++++++------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/onnxruntime/test/autoep/test_execution.cc b/onnxruntime/test/autoep/test_execution.cc index 27a00148a0d21..33ba08287d008 100644 --- a/onnxruntime/test/autoep/test_execution.cc +++ b/onnxruntime/test/autoep/test_execution.cc @@ -107,26 +107,40 @@ struct FakeEpContextConfigCallbacks { void* write_state = nullptr; }; -const OrtEpContextConfig* FakeEpContextConfig(FakeEpContextConfigCallbacks& callbacks) { - return reinterpret_cast(&callbacks); -} +// The low-level *WithFileFallback overloads treat the OrtEpContextConfig as an opaque token that is only forwarded +// to the injected getter; they never dereference it. These tests therefore pair a real (empty) OrtEpContextConfig +// with getters that return callbacks from this holder, instead of reinterpret_casting a foreign struct to +// OrtEpContextConfig* (which is undefined behavior). gtest runs these serially, so a single shared pointer is safe. +const FakeEpContextConfigCallbacks* g_fake_ep_context_callbacks = nullptr; -OrtStatus* ORT_API_CALL FakeEpContextConfigGetReadFunc(const OrtEpContextConfig* config, +OrtStatus* ORT_API_CALL FakeEpContextConfigGetReadFunc(const OrtEpContextConfig* /*config*/, OrtReadNamedBufferFunc* read_func, void** state) noexcept { - auto* callbacks = reinterpret_cast(config); - *read_func = callbacks->read_func; - *state = callbacks->read_state; + *read_func = g_fake_ep_context_callbacks->read_func; + *state = g_fake_ep_context_callbacks->read_state; return nullptr; } -OrtStatus* ORT_API_CALL FakeEpContextConfigGetWriteFunc(const OrtEpContextConfig* config, +OrtStatus* ORT_API_CALL FakeEpContextConfigGetWriteFunc(const OrtEpContextConfig* /*config*/, OrtWriteNamedBufferFunc* write_func, void** state) noexcept { - auto* callbacks = reinterpret_cast(config); - *write_func = callbacks->write_func; - *state = callbacks->write_state; + *write_func = g_fake_ep_context_callbacks->write_func; + *state = g_fake_ep_context_callbacks->write_state; return nullptr; } +// Returns a real, empty OrtEpContextConfig (owned by the returned wrapper) to use as the opaque token passed to the +// fake getters above. +Ort::Experimental::EpContextConfig MakeEmptyEpContextConfig() { + const auto& api = Ort::GetApi(); + auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&api); + EXPECT_NE(get_config, nullptr); + OrtEpContextConfig* raw = nullptr; + if (get_config != nullptr) { + Ort::SessionOptions session_options; + EXPECT_TRUE(Ort::Status{get_config(session_options, &raw)}.IsOK()); + } + return Ort::Experimental::EpContextConfig{&api, raw}; +} + // Invokes the experimental EPContext read setter on the public C API. void SetEpContextDataReadFunc(Ort::SessionOptions& session_options, OrtReadNamedBufferFunc read_func, void* state) { auto set_read_func = @@ -772,17 +786,19 @@ TEST(OrtEpLibrary, EpContextDataUtils_CallbackFallbackUsesCallbacks) { EpContextDataCallbackState write_callback_state; FakeEpContextConfigCallbacks callbacks{LoadEpContextDataCallback, &read_callback_state, StoreEpContextDataCallback, &write_callback_state}; + g_fake_ep_context_callbacks = &callbacks; + const Ort::Experimental::EpContextConfig fake_config = MakeEmptyEpContextConfig(); std::vector data; ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataWithFileFallback( - api, FakeEpContextConfigGetReadFunc, FakeEpContextConfig(callbacks), "callback_context.bin", nullptr, data)); + api, FakeEpContextConfigGetReadFunc, fake_config.get(), "callback_context.bin", nullptr, data)); ASSERT_TRUE(read_callback_state.read_called); EXPECT_EQ(read_callback_state.read_file_name, "callback_context.bin"); EXPECT_EQ(data, read_callback_state.payload); const std::string payload = "callback write payload"; ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( - api, FakeEpContextConfigGetWriteFunc, FakeEpContextConfig(callbacks), "callback_write_context.bin", nullptr, + api, FakeEpContextConfigGetWriteFunc, fake_config.get(), "callback_write_context.bin", nullptr, payload.data(), payload.size())); ASSERT_TRUE(write_callback_state.write_called); EXPECT_EQ(write_callback_state.write_file_name, "callback_write_context.bin"); @@ -791,7 +807,7 @@ TEST(OrtEpLibrary, EpContextDataUtils_CallbackFallbackUsesCallbacks) { write_callback_state = {}; const std::string payload_with_unused_fallback = "callback write payload with unused fallback"; ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( - api, FakeEpContextConfigGetWriteFunc, FakeEpContextConfig(callbacks), + api, FakeEpContextConfigGetWriteFunc, fake_config.get(), "callback_write_context_unused_fallback.bin", "", nullptr, payload_with_unused_fallback.data(), payload_with_unused_fallback.size())); ASSERT_TRUE(write_callback_state.write_called); @@ -805,10 +821,12 @@ TEST(OrtEpLibrary, EpContextDataUtils_ReadCallbackRejectsNullBufferForNonEmptyPa EpContextDataCallbackState read_callback_state; FakeEpContextConfigCallbacks callbacks{LoadInvalidEpContextDataCallback, &read_callback_state, nullptr, nullptr}; + g_fake_ep_context_callbacks = &callbacks; + const Ort::Experimental::EpContextConfig fake_config = MakeEmptyEpContextConfig(); std::vector data; ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataWithFileFallback( - api, FakeEpContextConfigGetReadFunc, FakeEpContextConfig(callbacks), + api, FakeEpContextConfigGetReadFunc, fake_config.get(), "invalid_callback_context.bin", nullptr, data), ORT_FAIL, "OrtReadNamedBufferFunc returned a null buffer for non-empty EPContext data"); ASSERT_TRUE(read_callback_state.read_called); From f610ce35e3b5d3181cac696281c3b068ded1866f Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Thu, 18 Jun 2026 16:53:23 -0700 Subject: [PATCH 27/48] Split EpContextDataUtils tests into their own file Moves the EpContextDataUtils_* unit tests out of test_execution.cc into ep_context_data_utils_test.cc, and extracts the EPContext read/write callback test doubles shared with the PluginEp tests into ep_context_data_callbacks.h. --- .../test/autoep/ep_context_data_callbacks.h | 61 ++++ .../test/autoep/ep_context_data_utils_test.cc | 283 +++++++++++++++++ onnxruntime/test/autoep/test_execution.cc | 294 +----------------- 3 files changed, 345 insertions(+), 293 deletions(-) create mode 100644 onnxruntime/test/autoep/ep_context_data_callbacks.h create mode 100644 onnxruntime/test/autoep/ep_context_data_utils_test.cc diff --git a/onnxruntime/test/autoep/ep_context_data_callbacks.h b/onnxruntime/test/autoep/ep_context_data_callbacks.h new file mode 100644 index 0000000000000..49472bc6dcb50 --- /dev/null +++ b/onnxruntime/test/autoep/ep_context_data_callbacks.h @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include +#include +#include +#include + +#include "core/session/onnxruntime_c_api.h" +#include "core/session/onnxruntime_cxx_api.h" + +namespace onnxruntime { +namespace test { + +// Shared EPContext read/write callback test doubles, used by both the EpContextDataUtils unit tests +// (ep_context_data_utils_test.cc) and the PluginEp end-to-end EPContext tests (test_execution.cc). +struct EpContextDataCallbackState { + bool write_called = false; + bool read_called = false; + std::string write_file_name; + std::string read_file_name; + std::vector payload; +}; + +inline OrtStatus* ORT_API_CALL StoreEpContextDataCallback(void* state, const char* file_name, const void* buffer, + size_t buffer_size) { + auto* callback_state = static_cast(state); + callback_state->write_called = true; + callback_state->write_file_name = file_name; + callback_state->payload.clear(); + if (buffer_size != 0) { + callback_state->payload.assign(static_cast(buffer), static_cast(buffer) + buffer_size); + } + return nullptr; +} + +inline OrtStatus* ORT_API_CALL LoadEpContextDataCallback(void* state, const char* file_name, OrtAllocator* allocator, + void** buffer, size_t* data_size) { + auto* callback_state = static_cast(state); + callback_state->read_called = true; + callback_state->read_file_name = file_name; + + *buffer = nullptr; + *data_size = callback_state->payload.size(); + if (callback_state->payload.empty()) { + return nullptr; + } + + OrtStatus* status = Ort::GetApi().AllocatorAlloc(allocator, callback_state->payload.size(), buffer); + if (status != nullptr) { + return status; + } + + std::copy(callback_state->payload.begin(), callback_state->payload.end(), static_cast(*buffer)); + return nullptr; +} + +} // namespace test +} // namespace onnxruntime diff --git a/onnxruntime/test/autoep/ep_context_data_utils_test.cc b/onnxruntime/test/autoep/ep_context_data_utils_test.cc new file mode 100644 index 0000000000000..088129fdad548 --- /dev/null +++ b/onnxruntime/test/autoep/ep_context_data_utils_test.cc @@ -0,0 +1,283 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Unit tests for the sample-only EPContext data helpers in +// onnxruntime/test/autoep/library/ep_context_data_utils.h. + +#include +#include +#include +#include + +#include +#include + +#include "core/session/onnxruntime_cxx_api.h" +#include "core/session/onnxruntime_experimental_c_api.h" + +#include "test/autoep/ep_context_data_callbacks.h" +#include "test/autoep/library/ep_context_data_utils.h" +#include "test/util/include/api_asserts.h" +#include "test/util/include/asserts.h" + +namespace onnxruntime { +namespace test { + +namespace { + +OrtStatus* ORT_API_CALL LoadInvalidEpContextDataCallback(void* state, const char* file_name, + OrtAllocator* /*allocator*/, void** buffer, + size_t* data_size) { + auto* callback_state = static_cast(state); + callback_state->read_called = true; + callback_state->read_file_name = file_name; + + *buffer = nullptr; + *data_size = 1; + return nullptr; +} + +void ExpectOrtStatusError(OrtStatus* status_ptr, OrtErrorCode expected_code, std::string_view expected_message) { + Ort::Status status{status_ptr}; + ASSERT_FALSE(status.IsOK()); + EXPECT_EQ(status.GetErrorCode(), expected_code); + EXPECT_THAT(std::string{status.GetErrorMessage()}, ::testing::HasSubstr(std::string{expected_message})); +} + +std::filesystem::path PrepareTempTestDir(std::string_view name) { + std::filesystem::path test_dir = std::string{name}; + std::filesystem::remove_all(test_dir); + std::filesystem::create_directories(test_dir); + return test_dir; +} + +struct FakeEpContextConfigCallbacks { + OrtReadNamedBufferFunc read_func = nullptr; + void* read_state = nullptr; + OrtWriteNamedBufferFunc write_func = nullptr; + void* write_state = nullptr; +}; + +// The low-level *WithFileFallback overloads treat the OrtEpContextConfig as an opaque token that is only forwarded +// to the injected getter; they never dereference it. These tests therefore pair a real (empty) OrtEpContextConfig +// with getters that return callbacks from this holder, instead of reinterpret_casting a foreign struct to +// OrtEpContextConfig* (which is undefined behavior). gtest runs these serially, so a single shared pointer is safe. +const FakeEpContextConfigCallbacks* g_fake_ep_context_callbacks = nullptr; + +OrtStatus* ORT_API_CALL FakeEpContextConfigGetReadFunc(const OrtEpContextConfig* /*config*/, + OrtReadNamedBufferFunc* read_func, void** state) noexcept { + *read_func = g_fake_ep_context_callbacks->read_func; + *state = g_fake_ep_context_callbacks->read_state; + return nullptr; +} + +OrtStatus* ORT_API_CALL FakeEpContextConfigGetWriteFunc(const OrtEpContextConfig* /*config*/, + OrtWriteNamedBufferFunc* write_func, void** state) noexcept { + *write_func = g_fake_ep_context_callbacks->write_func; + *state = g_fake_ep_context_callbacks->write_state; + return nullptr; +} + +// Returns a real, empty OrtEpContextConfig (owned by the returned wrapper) to use as the opaque token passed to the +// fake getters above. +Ort::Experimental::EpContextConfig MakeEmptyEpContextConfig() { + const auto& api = Ort::GetApi(); + auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&api); + EXPECT_NE(get_config, nullptr); + OrtEpContextConfig* raw = nullptr; + if (get_config != nullptr) { + Ort::SessionOptions session_options; + EXPECT_TRUE(Ort::Status{get_config(session_options, &raw)}.IsOK()); + } + return Ort::Experimental::EpContextConfig{&api, raw}; +} + +} // namespace + +TEST(OrtEpLibrary, EpContextDataUtils_PathHelpersRoundTrip) { + const std::string file_name = "context_data.bin"; + +#ifdef _WIN32 + const std::wstring wide_file_name = ep_context_data_utils::Utf8ToWideString(file_name); + ASSERT_FALSE(wide_file_name.empty()); + EXPECT_EQ(ep_context_data_utils::WideToUtf8String(wide_file_name), file_name); + + const std::string invalid_utf8(1, static_cast(0xff)); + EXPECT_TRUE(ep_context_data_utils::Utf8ToWideString(invalid_utf8).empty()); +#endif + + const std::filesystem::path file_path = ep_context_data_utils::Utf8Path(file_name.c_str()); + ASSERT_FALSE(file_path.empty()); + EXPECT_EQ(ep_context_data_utils::PathToUtf8String(file_path), file_name); + EXPECT_TRUE(ep_context_data_utils::Utf8Path(nullptr).empty()); + EXPECT_TRUE(ep_context_data_utils::Utf8Path("").empty()); +} + +TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathAndInvalidArguments) { + const auto& api = Ort::GetApi(); + std::filesystem::path data_path; + + data_path = "stale.ctx"; + ExpectOrtStatusError(ep_context_data_utils::ResolveEpContextDataPath(api, nullptr, nullptr, data_path), + ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); + EXPECT_TRUE(data_path.empty()); + + data_path = "stale.ctx"; + ExpectOrtStatusError(ep_context_data_utils::ResolveEpContextDataPath(api, "", nullptr, data_path), + ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); + EXPECT_TRUE(data_path.empty()); + + ASSERT_ORTSTATUS_OK(ep_context_data_utils::ResolveEpContextDataPath(api, "relative.ctx", nullptr, data_path)); + EXPECT_EQ(ep_context_data_utils::PathToUtf8String(data_path), "relative.ctx"); + + ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataToFile(api, "unused.ctx", nullptr, nullptr, 1), + ORT_INVALID_ARGUMENT, "EPContext data buffer must not be null for non-empty data"); + ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback(api, nullptr, "unused.ctx", nullptr, + nullptr, 1), + ORT_INVALID_ARGUMENT, "EPContext data buffer must not be null for non-empty data"); + + std::vector data; + ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataWithFileFallback(api, nullptr, "", nullptr, data), + ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); + ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback(api, nullptr, "", nullptr, nullptr, 0), + ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); + ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback( + api, nullptr, "logical_context_data.bin", "", nullptr, nullptr, 0), + ORT_INVALID_ARGUMENT, "EPContext data fallback file name must not be empty"); +} + +TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathRejectsUnsafeNames) { + const auto& api = Ort::GetApi(); + std::filesystem::path data_path; + + ExpectOrtStatusError(ep_context_data_utils::ResolveEpContextDataPath(api, "../escape.ctx", nullptr, data_path), + ORT_INVALID_ARGUMENT, "EPContext data file name must not contain path traversal"); + EXPECT_TRUE(data_path.empty()); + +#ifdef _WIN32 + const char* absolute_file_name = "C:\\temp\\escape.ctx"; +#else + const char* absolute_file_name = "/tmp/escape.ctx"; +#endif + ASSERT_ORTSTATUS_OK(ep_context_data_utils::ResolveEpContextDataPath(api, absolute_file_name, nullptr, data_path)); + EXPECT_TRUE(data_path.is_absolute()); + + std::vector data; + ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataFromFile(api, "../escape.ctx", nullptr, data), + ORT_INVALID_ARGUMENT, "EPContext data file name must not contain path traversal"); + ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback( + api, nullptr, absolute_file_name, "unused.ctx", nullptr, nullptr, 0), + ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute"); +} + +TEST(OrtEpLibrary, EpContextDataUtils_FileFallbackReadsAndWrites) { + const auto& api = Ort::GetApi(); + const std::filesystem::path test_dir = PrepareTempTestDir("ort_ep_context_data_utils_file_fallback_test"); + auto cleanup = gsl::finally([&]() { std::filesystem::remove_all(test_dir); }); + + const std::string payload = "file fallback payload"; + const std::filesystem::path data_path = test_dir / "context_data.bin"; + const std::string data_file_name = ep_context_data_utils::PathToUtf8String(data_path); + + ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataToFile(api, data_file_name.c_str(), nullptr, + payload.data(), payload.size())); + + std::vector data; + ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataFromFile(api, data_file_name.c_str(), nullptr, data)); + EXPECT_EQ(std::string(data.begin(), data.end()), payload); + + const std::filesystem::path wrapper_data_path = test_dir / "wrapper_context_data.bin"; + const std::string wrapper_data_file_name = ep_context_data_utils::PathToUtf8String(wrapper_data_path); + ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( + api, nullptr, wrapper_data_file_name.c_str(), nullptr, payload.data(), payload.size())); + + data.clear(); + ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataWithFileFallback( + api, nullptr, wrapper_data_file_name.c_str(), nullptr, data)); + EXPECT_EQ(std::string(data.begin(), data.end()), payload); + + const std::filesystem::path fallback_data_path = test_dir / "fallback_context_data.bin"; + const std::string fallback_data_file_name = ep_context_data_utils::PathToUtf8String(fallback_data_path); + ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( + api, nullptr, "logical_context_data.bin", fallback_data_file_name.c_str(), nullptr, payload.data(), + payload.size())); + + data.clear(); + ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataFromFile(api, fallback_data_file_name.c_str(), nullptr, + data)); + EXPECT_EQ(std::string(data.begin(), data.end()), payload); + + const std::filesystem::path empty_data_path = test_dir / "empty_context_data.bin"; + const std::string empty_data_file_name = ep_context_data_utils::PathToUtf8String(empty_data_path); + ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( + api, nullptr, empty_data_file_name.c_str(), nullptr, nullptr, 0)); + + data.assign({'s', 't', 'a', 'l', 'e'}); + ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataWithFileFallback( + api, nullptr, empty_data_file_name.c_str(), nullptr, data)); + EXPECT_TRUE(data.empty()); + + const std::filesystem::path missing_data_path = test_dir / "missing_context_data.bin"; + const std::string missing_data_file_name = ep_context_data_utils::PathToUtf8String(missing_data_path); + ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataFromFile(api, missing_data_file_name.c_str(), nullptr, + data), + ORT_FAIL, "Failed to open EPContext data file for read"); +} + +TEST(OrtEpLibrary, EpContextDataUtils_CallbackFallbackUsesCallbacks) { + const auto& api = Ort::GetApi(); + + EpContextDataCallbackState read_callback_state; + read_callback_state.payload = {'c', 'a', 'l', 'l', 'b', 'a', 'c', 'k'}; + EpContextDataCallbackState write_callback_state; + FakeEpContextConfigCallbacks callbacks{LoadEpContextDataCallback, &read_callback_state, + StoreEpContextDataCallback, &write_callback_state}; + g_fake_ep_context_callbacks = &callbacks; + const Ort::Experimental::EpContextConfig fake_config = MakeEmptyEpContextConfig(); + + std::vector data; + ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataWithFileFallback( + api, FakeEpContextConfigGetReadFunc, fake_config.get(), "callback_context.bin", nullptr, data)); + ASSERT_TRUE(read_callback_state.read_called); + EXPECT_EQ(read_callback_state.read_file_name, "callback_context.bin"); + EXPECT_EQ(data, read_callback_state.payload); + + const std::string payload = "callback write payload"; + ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( + api, FakeEpContextConfigGetWriteFunc, fake_config.get(), "callback_write_context.bin", nullptr, + payload.data(), payload.size())); + ASSERT_TRUE(write_callback_state.write_called); + EXPECT_EQ(write_callback_state.write_file_name, "callback_write_context.bin"); + EXPECT_EQ(std::string(write_callback_state.payload.begin(), write_callback_state.payload.end()), payload); + + write_callback_state = {}; + const std::string payload_with_unused_fallback = "callback write payload with unused fallback"; + ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( + api, FakeEpContextConfigGetWriteFunc, fake_config.get(), + "callback_write_context_unused_fallback.bin", "", nullptr, + payload_with_unused_fallback.data(), payload_with_unused_fallback.size())); + ASSERT_TRUE(write_callback_state.write_called); + EXPECT_EQ(write_callback_state.write_file_name, "callback_write_context_unused_fallback.bin"); + EXPECT_EQ(std::string(write_callback_state.payload.begin(), write_callback_state.payload.end()), + payload_with_unused_fallback); +} + +TEST(OrtEpLibrary, EpContextDataUtils_ReadCallbackRejectsNullBufferForNonEmptyPayload) { + const auto& api = Ort::GetApi(); + + EpContextDataCallbackState read_callback_state; + FakeEpContextConfigCallbacks callbacks{LoadInvalidEpContextDataCallback, &read_callback_state, nullptr, nullptr}; + g_fake_ep_context_callbacks = &callbacks; + const Ort::Experimental::EpContextConfig fake_config = MakeEmptyEpContextConfig(); + + std::vector data; + ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataWithFileFallback( + api, FakeEpContextConfigGetReadFunc, fake_config.get(), + "invalid_callback_context.bin", nullptr, data), + ORT_FAIL, "OrtReadNamedBufferFunc returned a null buffer for non-empty EPContext data"); + ASSERT_TRUE(read_callback_state.read_called); + EXPECT_EQ(read_callback_state.read_file_name, "invalid_callback_context.bin"); +} + +} // namespace test +} // namespace onnxruntime diff --git a/onnxruntime/test/autoep/test_execution.cc b/onnxruntime/test/autoep/test_execution.cc index 33ba08287d008..4584de78ea008 100644 --- a/onnxruntime/test/autoep/test_execution.cc +++ b/onnxruntime/test/autoep/test_execution.cc @@ -19,6 +19,7 @@ #include "core/session/onnxruntime_ep_device_ep_metadata_keys.h" #include "nlohmann/json.hpp" +#include "test/autoep/ep_context_data_callbacks.h" #include "test/autoep/test_autoep_utils.h" #include "test/autoep/library/ep_context_data_utils.h" #include "test/autoep/library/example_plugin_ep/ep_test_hooks.h" @@ -33,114 +34,6 @@ namespace test { namespace { -struct EpContextDataCallbackState { - bool write_called = false; - bool read_called = false; - std::string write_file_name; - std::string read_file_name; - std::vector payload; -}; - -OrtStatus* ORT_API_CALL StoreEpContextDataCallback(void* state, const char* file_name, const void* buffer, - size_t buffer_size) { - auto* callback_state = static_cast(state); - callback_state->write_called = true; - callback_state->write_file_name = file_name; - callback_state->payload.clear(); - if (buffer_size != 0) { - callback_state->payload.assign(static_cast(buffer), static_cast(buffer) + buffer_size); - } - return nullptr; -} - -OrtStatus* ORT_API_CALL LoadEpContextDataCallback(void* state, const char* file_name, OrtAllocator* allocator, - void** buffer, size_t* data_size) { - auto* callback_state = static_cast(state); - callback_state->read_called = true; - callback_state->read_file_name = file_name; - - *buffer = nullptr; - *data_size = callback_state->payload.size(); - if (callback_state->payload.empty()) { - return nullptr; - } - - OrtStatus* status = Ort::GetApi().AllocatorAlloc(allocator, callback_state->payload.size(), buffer); - if (status != nullptr) { - return status; - } - - std::copy(callback_state->payload.begin(), callback_state->payload.end(), static_cast(*buffer)); - return nullptr; -} - -OrtStatus* ORT_API_CALL LoadInvalidEpContextDataCallback(void* state, const char* file_name, - OrtAllocator* /*allocator*/, void** buffer, - size_t* data_size) { - auto* callback_state = static_cast(state); - callback_state->read_called = true; - callback_state->read_file_name = file_name; - - *buffer = nullptr; - *data_size = 1; - return nullptr; -} - -void ExpectOrtStatusError(OrtStatus* status_ptr, OrtErrorCode expected_code, std::string_view expected_message) { - Ort::Status status{status_ptr}; - ASSERT_FALSE(status.IsOK()); - EXPECT_EQ(status.GetErrorCode(), expected_code); - EXPECT_THAT(std::string{status.GetErrorMessage()}, ::testing::HasSubstr(std::string{expected_message})); -} - -std::filesystem::path PrepareTempTestDir(std::string_view name) { - std::filesystem::path test_dir = std::string{name}; - std::filesystem::remove_all(test_dir); - std::filesystem::create_directories(test_dir); - return test_dir; -} - -struct FakeEpContextConfigCallbacks { - OrtReadNamedBufferFunc read_func = nullptr; - void* read_state = nullptr; - OrtWriteNamedBufferFunc write_func = nullptr; - void* write_state = nullptr; -}; - -// The low-level *WithFileFallback overloads treat the OrtEpContextConfig as an opaque token that is only forwarded -// to the injected getter; they never dereference it. These tests therefore pair a real (empty) OrtEpContextConfig -// with getters that return callbacks from this holder, instead of reinterpret_casting a foreign struct to -// OrtEpContextConfig* (which is undefined behavior). gtest runs these serially, so a single shared pointer is safe. -const FakeEpContextConfigCallbacks* g_fake_ep_context_callbacks = nullptr; - -OrtStatus* ORT_API_CALL FakeEpContextConfigGetReadFunc(const OrtEpContextConfig* /*config*/, - OrtReadNamedBufferFunc* read_func, void** state) noexcept { - *read_func = g_fake_ep_context_callbacks->read_func; - *state = g_fake_ep_context_callbacks->read_state; - return nullptr; -} - -OrtStatus* ORT_API_CALL FakeEpContextConfigGetWriteFunc(const OrtEpContextConfig* /*config*/, - OrtWriteNamedBufferFunc* write_func, void** state) noexcept { - *write_func = g_fake_ep_context_callbacks->write_func; - *state = g_fake_ep_context_callbacks->write_state; - return nullptr; -} - -// Returns a real, empty OrtEpContextConfig (owned by the returned wrapper) to use as the opaque token passed to the -// fake getters above. -Ort::Experimental::EpContextConfig MakeEmptyEpContextConfig() { - const auto& api = Ort::GetApi(); - auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&api); - EXPECT_NE(get_config, nullptr); - OrtEpContextConfig* raw = nullptr; - if (get_config != nullptr) { - Ort::SessionOptions session_options; - EXPECT_TRUE(Ort::Status{get_config(session_options, &raw)}.IsOK()); - } - return Ort::Experimental::EpContextConfig{&api, raw}; -} - // Invokes the experimental EPContext read setter on the public C API. void SetEpContextDataReadFunc(Ort::SessionOptions& session_options, OrtReadNamedBufferFunc read_func, void* state) { auto set_read_func = @@ -648,191 +541,6 @@ TEST(OrtEpLibrary, PluginEp_AppendV2_Fp16HardSigmoid_EpGraphAssignmentInfo) { ASSERT_EQ(ep_nodes[0].GetName(), std::string("hardsigmoid_0")); } -TEST(OrtEpLibrary, EpContextDataUtils_PathHelpersRoundTrip) { - const std::string file_name = "context_data.bin"; - -#ifdef _WIN32 - const std::wstring wide_file_name = ep_context_data_utils::Utf8ToWideString(file_name); - ASSERT_FALSE(wide_file_name.empty()); - EXPECT_EQ(ep_context_data_utils::WideToUtf8String(wide_file_name), file_name); - - const std::string invalid_utf8(1, static_cast(0xff)); - EXPECT_TRUE(ep_context_data_utils::Utf8ToWideString(invalid_utf8).empty()); -#endif - - const std::filesystem::path file_path = ep_context_data_utils::Utf8Path(file_name.c_str()); - ASSERT_FALSE(file_path.empty()); - EXPECT_EQ(ep_context_data_utils::PathToUtf8String(file_path), file_name); - EXPECT_TRUE(ep_context_data_utils::Utf8Path(nullptr).empty()); - EXPECT_TRUE(ep_context_data_utils::Utf8Path("").empty()); -} - -TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathAndInvalidArguments) { - const auto& api = Ort::GetApi(); - std::filesystem::path data_path; - - data_path = "stale.ctx"; - ExpectOrtStatusError(ep_context_data_utils::ResolveEpContextDataPath(api, nullptr, nullptr, data_path), - ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); - EXPECT_TRUE(data_path.empty()); - - data_path = "stale.ctx"; - ExpectOrtStatusError(ep_context_data_utils::ResolveEpContextDataPath(api, "", nullptr, data_path), - ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); - EXPECT_TRUE(data_path.empty()); - - ASSERT_ORTSTATUS_OK(ep_context_data_utils::ResolveEpContextDataPath(api, "relative.ctx", nullptr, data_path)); - EXPECT_EQ(ep_context_data_utils::PathToUtf8String(data_path), "relative.ctx"); - - ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataToFile(api, "unused.ctx", nullptr, nullptr, 1), - ORT_INVALID_ARGUMENT, "EPContext data buffer must not be null for non-empty data"); - ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback(api, nullptr, "unused.ctx", nullptr, - nullptr, 1), - ORT_INVALID_ARGUMENT, "EPContext data buffer must not be null for non-empty data"); - - std::vector data; - ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataWithFileFallback(api, nullptr, "", nullptr, data), - ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); - ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback(api, nullptr, "", nullptr, nullptr, 0), - ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); - ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback( - api, nullptr, "logical_context_data.bin", "", nullptr, nullptr, 0), - ORT_INVALID_ARGUMENT, "EPContext data fallback file name must not be empty"); -} - -TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathRejectsUnsafeNames) { - const auto& api = Ort::GetApi(); - std::filesystem::path data_path; - - ExpectOrtStatusError(ep_context_data_utils::ResolveEpContextDataPath(api, "../escape.ctx", nullptr, data_path), - ORT_INVALID_ARGUMENT, "EPContext data file name must not contain path traversal"); - EXPECT_TRUE(data_path.empty()); - -#ifdef _WIN32 - const char* absolute_file_name = "C:\\temp\\escape.ctx"; -#else - const char* absolute_file_name = "/tmp/escape.ctx"; -#endif - ASSERT_ORTSTATUS_OK(ep_context_data_utils::ResolveEpContextDataPath(api, absolute_file_name, nullptr, data_path)); - EXPECT_TRUE(data_path.is_absolute()); - - std::vector data; - ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataFromFile(api, "../escape.ctx", nullptr, data), - ORT_INVALID_ARGUMENT, "EPContext data file name must not contain path traversal"); - ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback( - api, nullptr, absolute_file_name, "unused.ctx", nullptr, nullptr, 0), - ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute"); -} - -TEST(OrtEpLibrary, EpContextDataUtils_FileFallbackReadsAndWrites) { - const auto& api = Ort::GetApi(); - const std::filesystem::path test_dir = PrepareTempTestDir("ort_ep_context_data_utils_file_fallback_test"); - auto cleanup = gsl::finally([&]() { std::filesystem::remove_all(test_dir); }); - - const std::string payload = "file fallback payload"; - const std::filesystem::path data_path = test_dir / "context_data.bin"; - const std::string data_file_name = ep_context_data_utils::PathToUtf8String(data_path); - - ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataToFile(api, data_file_name.c_str(), nullptr, - payload.data(), payload.size())); - - std::vector data; - ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataFromFile(api, data_file_name.c_str(), nullptr, data)); - EXPECT_EQ(std::string(data.begin(), data.end()), payload); - - const std::filesystem::path wrapper_data_path = test_dir / "wrapper_context_data.bin"; - const std::string wrapper_data_file_name = ep_context_data_utils::PathToUtf8String(wrapper_data_path); - ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( - api, nullptr, wrapper_data_file_name.c_str(), nullptr, payload.data(), payload.size())); - - data.clear(); - ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataWithFileFallback( - api, nullptr, wrapper_data_file_name.c_str(), nullptr, data)); - EXPECT_EQ(std::string(data.begin(), data.end()), payload); - - const std::filesystem::path fallback_data_path = test_dir / "fallback_context_data.bin"; - const std::string fallback_data_file_name = ep_context_data_utils::PathToUtf8String(fallback_data_path); - ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( - api, nullptr, "logical_context_data.bin", fallback_data_file_name.c_str(), nullptr, payload.data(), - payload.size())); - - data.clear(); - ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataFromFile(api, fallback_data_file_name.c_str(), nullptr, - data)); - EXPECT_EQ(std::string(data.begin(), data.end()), payload); - - const std::filesystem::path empty_data_path = test_dir / "empty_context_data.bin"; - const std::string empty_data_file_name = ep_context_data_utils::PathToUtf8String(empty_data_path); - ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( - api, nullptr, empty_data_file_name.c_str(), nullptr, nullptr, 0)); - - data.assign({'s', 't', 'a', 'l', 'e'}); - ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataWithFileFallback( - api, nullptr, empty_data_file_name.c_str(), nullptr, data)); - EXPECT_TRUE(data.empty()); - - const std::filesystem::path missing_data_path = test_dir / "missing_context_data.bin"; - const std::string missing_data_file_name = ep_context_data_utils::PathToUtf8String(missing_data_path); - ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataFromFile(api, missing_data_file_name.c_str(), nullptr, - data), - ORT_FAIL, "Failed to open EPContext data file for read"); -} - -TEST(OrtEpLibrary, EpContextDataUtils_CallbackFallbackUsesCallbacks) { - const auto& api = Ort::GetApi(); - - EpContextDataCallbackState read_callback_state; - read_callback_state.payload = {'c', 'a', 'l', 'l', 'b', 'a', 'c', 'k'}; - EpContextDataCallbackState write_callback_state; - FakeEpContextConfigCallbacks callbacks{LoadEpContextDataCallback, &read_callback_state, - StoreEpContextDataCallback, &write_callback_state}; - g_fake_ep_context_callbacks = &callbacks; - const Ort::Experimental::EpContextConfig fake_config = MakeEmptyEpContextConfig(); - - std::vector data; - ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataWithFileFallback( - api, FakeEpContextConfigGetReadFunc, fake_config.get(), "callback_context.bin", nullptr, data)); - ASSERT_TRUE(read_callback_state.read_called); - EXPECT_EQ(read_callback_state.read_file_name, "callback_context.bin"); - EXPECT_EQ(data, read_callback_state.payload); - - const std::string payload = "callback write payload"; - ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( - api, FakeEpContextConfigGetWriteFunc, fake_config.get(), "callback_write_context.bin", nullptr, - payload.data(), payload.size())); - ASSERT_TRUE(write_callback_state.write_called); - EXPECT_EQ(write_callback_state.write_file_name, "callback_write_context.bin"); - EXPECT_EQ(std::string(write_callback_state.payload.begin(), write_callback_state.payload.end()), payload); - - write_callback_state = {}; - const std::string payload_with_unused_fallback = "callback write payload with unused fallback"; - ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( - api, FakeEpContextConfigGetWriteFunc, fake_config.get(), - "callback_write_context_unused_fallback.bin", "", nullptr, - payload_with_unused_fallback.data(), payload_with_unused_fallback.size())); - ASSERT_TRUE(write_callback_state.write_called); - EXPECT_EQ(write_callback_state.write_file_name, "callback_write_context_unused_fallback.bin"); - EXPECT_EQ(std::string(write_callback_state.payload.begin(), write_callback_state.payload.end()), - payload_with_unused_fallback); -} - -TEST(OrtEpLibrary, EpContextDataUtils_ReadCallbackRejectsNullBufferForNonEmptyPayload) { - const auto& api = Ort::GetApi(); - - EpContextDataCallbackState read_callback_state; - FakeEpContextConfigCallbacks callbacks{LoadInvalidEpContextDataCallback, &read_callback_state, nullptr, nullptr}; - g_fake_ep_context_callbacks = &callbacks; - const Ort::Experimental::EpContextConfig fake_config = MakeEmptyEpContextConfig(); - - std::vector data; - ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataWithFileFallback( - api, FakeEpContextConfigGetReadFunc, fake_config.get(), - "invalid_callback_context.bin", nullptr, data), - ORT_FAIL, "OrtReadNamedBufferFunc returned a null buffer for non-empty EPContext data"); - ASSERT_TRUE(read_callback_state.read_called); - EXPECT_EQ(read_callback_state.read_file_name, "invalid_callback_context.bin"); -} - // Generate an EPContext model with a plugin EP. // This test uses the OrtCompileApi but could also be done by setting the appropriate session option configs. TEST(OrtEpLibrary, PluginEp_GenEpContextModel) { From 5bef55c14859c69f94f3cec22959a06182169286 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Thu, 18 Jun 2026 19:06:04 -0700 Subject: [PATCH 28/48] Address Copilot review: reset fake-config global, wrap long line, clarify overload doc - Reset g_fake_ep_context_callbacks to nullptr via gsl::finally in both EpContextDataUtils callback tests so the global never dangles at a stack-local after the test. - Wrap the >120-char SetEpContextDataWriteFunc experimental-getter line. - Document that the WriteEpContextDataWithFileFallback convenience overload requires a safe relative file_name and that absolute physical paths need the explicit fallback_file_name overload. --- onnxruntime/test/autoep/ep_context_data_utils_test.cc | 2 ++ onnxruntime/test/autoep/library/ep_context_data_utils.h | 3 +++ onnxruntime/test/autoep/test_execution.cc | 3 ++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/onnxruntime/test/autoep/ep_context_data_utils_test.cc b/onnxruntime/test/autoep/ep_context_data_utils_test.cc index 088129fdad548..4c17958669209 100644 --- a/onnxruntime/test/autoep/ep_context_data_utils_test.cc +++ b/onnxruntime/test/autoep/ep_context_data_utils_test.cc @@ -233,6 +233,7 @@ TEST(OrtEpLibrary, EpContextDataUtils_CallbackFallbackUsesCallbacks) { FakeEpContextConfigCallbacks callbacks{LoadEpContextDataCallback, &read_callback_state, StoreEpContextDataCallback, &write_callback_state}; g_fake_ep_context_callbacks = &callbacks; + auto reset_fake_callbacks = gsl::finally([]() { g_fake_ep_context_callbacks = nullptr; }); const Ort::Experimental::EpContextConfig fake_config = MakeEmptyEpContextConfig(); std::vector data; @@ -268,6 +269,7 @@ TEST(OrtEpLibrary, EpContextDataUtils_ReadCallbackRejectsNullBufferForNonEmptyPa EpContextDataCallbackState read_callback_state; FakeEpContextConfigCallbacks callbacks{LoadInvalidEpContextDataCallback, &read_callback_state, nullptr, nullptr}; g_fake_ep_context_callbacks = &callbacks; + auto reset_fake_callbacks = gsl::finally([]() { g_fake_ep_context_callbacks = nullptr; }); const Ort::Experimental::EpContextConfig fake_config = MakeEmptyEpContextConfig(); std::vector data; diff --git a/onnxruntime/test/autoep/library/ep_context_data_utils.h b/onnxruntime/test/autoep/library/ep_context_data_utils.h index 35aecf1d4b1a2..87974f997b5db 100644 --- a/onnxruntime/test/autoep/library/ep_context_data_utils.h +++ b/onnxruntime/test/autoep/library/ep_context_data_utils.h @@ -406,6 +406,9 @@ inline OrtStatus* WriteEpContextDataWithFileFallback( } // Convenience overload that uses `file_name` as both the logical callback name and the file-fallback path. +// Because `file_name` doubles as the fallback path, it must be a safe relative name (this overload validates it and +// rejects absolute paths and `..` traversal). To write the file fallback to an absolute physical path (a trusted +// caller with `graph == nullptr`), use the overload above that takes a separate `fallback_file_name`. inline OrtStatus* WriteEpContextDataWithFileFallback( const OrtApi& api, const OrtEpContextConfig* ep_context_config, diff --git a/onnxruntime/test/autoep/test_execution.cc b/onnxruntime/test/autoep/test_execution.cc index 4584de78ea008..03d13d2e18a0a 100644 --- a/onnxruntime/test/autoep/test_execution.cc +++ b/onnxruntime/test/autoep/test_execution.cc @@ -46,7 +46,8 @@ void SetEpContextDataReadFunc(Ort::SessionOptions& session_options, OrtReadNamed void SetEpContextDataWriteFunc(Ort::ModelCompilationOptions& compile_options, OrtWriteNamedBufferFunc write_func, void* state) { auto set_write_func = - Ort::Experimental::Get_OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28_Fn(&Ort::GetApi()); + Ort::Experimental::Get_OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28_Fn( + &Ort::GetApi()); ASSERT_NE(set_write_func, nullptr); ASSERT_ORTSTATUS_OK(set_write_func(compile_options, write_func, state)); } From c8fef6db85d0124f34296795ee3a75a870c61a8b Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Fri, 19 Jun 2026 14:26:17 -0700 Subject: [PATCH 29/48] Reject Windows rooted EPContext data names Treat rooted-but-not-fully-qualified Windows path forms such as C:escape.ctx and \\escape.ctx as unsafe for EPContext logical/model-derived names. Applies the check in ValidateEpContextDataName and the graph-backed read/write resolvers, updates helper comments, and adds _WIN32 tests for drive-relative and root-relative names. --- .../test/autoep/ep_context_data_utils_test.cc | 13 ++++++- .../autoep/library/ep_context_data_utils.h | 34 +++++++++++-------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/onnxruntime/test/autoep/ep_context_data_utils_test.cc b/onnxruntime/test/autoep/ep_context_data_utils_test.cc index 4c17958669209..91fefefcf6de3 100644 --- a/onnxruntime/test/autoep/ep_context_data_utils_test.cc +++ b/onnxruntime/test/autoep/ep_context_data_utils_test.cc @@ -156,18 +156,29 @@ TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathRejectsUnsafeNames) { #ifdef _WIN32 const char* absolute_file_name = "C:\\temp\\escape.ctx"; + const char* drive_relative_file_name = "C:escape.ctx"; + const char* root_relative_file_name = "\\escape.ctx"; #else const char* absolute_file_name = "/tmp/escape.ctx"; #endif ASSERT_ORTSTATUS_OK(ep_context_data_utils::ResolveEpContextDataPath(api, absolute_file_name, nullptr, data_path)); EXPECT_TRUE(data_path.is_absolute()); +#ifdef _WIN32 + ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback( + api, nullptr, drive_relative_file_name, "unused.ctx", nullptr, nullptr, 0), + ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute or rooted"); + ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback( + api, nullptr, root_relative_file_name, "unused.ctx", nullptr, nullptr, 0), + ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute or rooted"); +#endif + std::vector data; ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataFromFile(api, "../escape.ctx", nullptr, data), ORT_INVALID_ARGUMENT, "EPContext data file name must not contain path traversal"); ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback( api, nullptr, absolute_file_name, "unused.ctx", nullptr, nullptr, 0), - ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute"); + ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute or rooted"); } TEST(OrtEpLibrary, EpContextDataUtils_FileFallbackReadsAndWrites) { diff --git a/onnxruntime/test/autoep/library/ep_context_data_utils.h b/onnxruntime/test/autoep/library/ep_context_data_utils.h index 87974f997b5db..23633d77afd21 100644 --- a/onnxruntime/test/autoep/library/ep_context_data_utils.h +++ b/onnxruntime/test/autoep/library/ep_context_data_utils.h @@ -104,6 +104,10 @@ inline bool ContainsPathTraversal(const std::filesystem::path& path) { return false; } +inline bool HasAbsoluteOrRootedPath(const std::filesystem::path& path) { + return path.is_absolute() || path.has_root_name() || path.has_root_directory(); +} + inline OrtStatus* ValidateEpContextDataName(const OrtApi& api, const char* file_name, std::filesystem::path& data_name) { data_name.clear(); @@ -117,8 +121,8 @@ inline OrtStatus* ValidateEpContextDataName(const OrtApi& api, const char* file_ return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name is not a valid path"); } - if (candidate_path.is_absolute()) { - return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute"); + if (HasAbsoluteOrRootedPath(candidate_path)) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute or rooted"); } if (ContainsPathTraversal(candidate_path)) { @@ -154,8 +158,8 @@ inline OrtStatus* ResolveEpContextDataPath(const OrtApi& api, const char* file_n return nullptr; } - if (candidate_path.is_absolute()) { - return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute"); + if (HasAbsoluteOrRootedPath(candidate_path)) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute or rooted"); } const ORTCHAR_T* model_path = nullptr; @@ -186,14 +190,14 @@ inline OrtStatus* ResolveEpContextDataOutputPath(const OrtApi& api, const char* } // Trusted direct callers (graph == nullptr) may supply an absolute physical path. When a graph is present, the - // name may be model-derived (untrusted), so reject absolute paths for symmetry with the read-side - // ResolveEpContextDataPath instead of writing to an attacker-chosen absolute location. + // name may be model-derived (untrusted), so reject absolute/rooted paths for symmetry with the read-side + // ResolveEpContextDataPath instead of writing to an attacker-chosen absolute or rooted location. if (graph == nullptr) { return nullptr; } - if (data_path.is_absolute()) { - return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute"); + if (HasAbsoluteOrRootedPath(data_path)) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute or rooted"); } const ORTCHAR_T* model_path = nullptr; @@ -321,9 +325,9 @@ inline OrtStatus* ReadEpContextDataWithFileFallback( // Reads EPContext binary data named `file_name`. If the session configured an OrtReadNamedBufferFunc (carried by // `ep_context_config`), it is used; otherwise the data is read from a file. When `graph` is non-null it is the -// EPContext model graph: untrusted absolute/traversal names are rejected and relative names are resolved against the -// model directory. Pass `graph == nullptr` only for trusted callers supplying a physical path. `data` is cleared -// first and receives the bytes on success. +// EPContext model graph: untrusted absolute/rooted/traversal names are rejected and relative names are resolved +// against the model directory. Pass `graph == nullptr` only for trusted callers supplying a physical path. `data` is +// cleared first and receives the bytes on success. inline OrtStatus* ReadEpContextDataWithFileFallback( const OrtApi& api, const OrtEpContextConfig* ep_context_config, @@ -381,8 +385,8 @@ inline OrtStatus* WriteEpContextDataWithFileFallback( // Writes EPContext binary data. If the compilation configured an OrtWriteNamedBufferFunc (carried by // `ep_context_config`), it is used and `file_name` is passed through unmodified as the logical name. Otherwise the // data is written to a file at `fallback_file_name`, which is resolved against the model directory when `graph` is -// non-null (and rejected if absolute in that case). `graph == nullptr` denotes a trusted caller that may supply an -// absolute physical path. `buffer` may be null only when `buffer_size` is 0. +// non-null (and rejected if absolute or rooted in that case). `graph == nullptr` denotes a trusted caller that may +// supply an absolute physical path. `buffer` may be null only when `buffer_size` is 0. inline OrtStatus* WriteEpContextDataWithFileFallback( const OrtApi& api, const OrtEpContextConfig* ep_context_config, @@ -407,8 +411,8 @@ inline OrtStatus* WriteEpContextDataWithFileFallback( // Convenience overload that uses `file_name` as both the logical callback name and the file-fallback path. // Because `file_name` doubles as the fallback path, it must be a safe relative name (this overload validates it and -// rejects absolute paths and `..` traversal). To write the file fallback to an absolute physical path (a trusted -// caller with `graph == nullptr`), use the overload above that takes a separate `fallback_file_name`. +// rejects absolute/rooted paths and `..` traversal). To write the file fallback to an absolute physical path (a +// trusted caller with `graph == nullptr`), use the overload above that takes a separate `fallback_file_name`. inline OrtStatus* WriteEpContextDataWithFileFallback( const OrtApi& api, const OrtEpContextConfig* ep_context_config, From d3576aacfa2d524d23f1d49923bce6b8397c1e0f Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Fri, 19 Jun 2026 14:59:37 -0700 Subject: [PATCH 30/48] Fix clang-format alignment in EpContextDataUtils _WIN32 test block --- onnxruntime/test/autoep/ep_context_data_utils_test.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/onnxruntime/test/autoep/ep_context_data_utils_test.cc b/onnxruntime/test/autoep/ep_context_data_utils_test.cc index 91fefefcf6de3..50daa545f5c8d 100644 --- a/onnxruntime/test/autoep/ep_context_data_utils_test.cc +++ b/onnxruntime/test/autoep/ep_context_data_utils_test.cc @@ -167,10 +167,10 @@ TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathRejectsUnsafeNames) { #ifdef _WIN32 ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback( api, nullptr, drive_relative_file_name, "unused.ctx", nullptr, nullptr, 0), - ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute or rooted"); + ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute or rooted"); ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback( api, nullptr, root_relative_file_name, "unused.ctx", nullptr, nullptr, 0), - ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute or rooted"); + ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute or rooted"); #endif std::vector data; @@ -178,7 +178,7 @@ TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathRejectsUnsafeNames) { ORT_INVALID_ARGUMENT, "EPContext data file name must not contain path traversal"); ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback( api, nullptr, absolute_file_name, "unused.ctx", nullptr, nullptr, 0), - ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute or rooted"); + ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute or rooted"); } TEST(OrtEpLibrary, EpContextDataUtils_FileFallbackReadsAndWrites) { From dcdb5f62229c548248abd25726f6ab21ed590823 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Fri, 19 Jun 2026 16:19:59 -0700 Subject: [PATCH 31/48] Address review: assert non-null status in test helper; verify ReleaseEpContextConfig availability before creating handle --- .../test/autoep/library/example_plugin_ep/ep_factory.cc | 6 ++++++ onnxruntime/test/framework/ep_plugin_provider_test.cc | 1 + 2 files changed, 7 insertions(+) diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc index 953961da3ef7e..5592507c18678 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc @@ -244,6 +244,12 @@ OrtStatus* ORT_API_CALL ExampleEpFactory::CreateEpImpl(OrtEpFactory* this_ptr, return factory->ort_api.CreateStatus(ORT_NOT_IMPLEMENTED, "OrtEpApi_SessionOptions_GetEpContextConfig is not available"); } + // Ensure the matching release function is available before creating a handle so the RAII wrapper can free it. + // Otherwise GetEpContextConfig would return a handle that can never be released (silent leak). + if (Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&factory->ort_api) == nullptr) { + return factory->ort_api.CreateStatus(ORT_NOT_IMPLEMENTED, + "OrtEpApi_ReleaseEpContextConfig is not available"); + } RETURN_IF_ERROR(get_ep_context_config(session_options, &ep_context_config)); // ExampleEp takes ownership of the config via the RAII wrapper; if construction throws, the temporary wrapper diff --git a/onnxruntime/test/framework/ep_plugin_provider_test.cc b/onnxruntime/test/framework/ep_plugin_provider_test.cc index 5d2862d90e98c..ab613e3c2f3a1 100644 --- a/onnxruntime/test/framework/ep_plugin_provider_test.cc +++ b/onnxruntime/test/framework/ep_plugin_provider_test.cc @@ -67,6 +67,7 @@ static void CheckFileIsEmpty(const PathString& filename) { // the OrtStatus* returned directly by the API under test without releasing it themselves. static void ExpectFailureOrtStatus(OrtStatus* status_ptr, OrtErrorCode expected_code, const char* expected_message) { Ort::Status status{status_ptr}; + ASSERT_NE(status_ptr, nullptr) << "Expected a failure status, but the API returned nullptr (OK)."; ASSERT_FALSE(status.IsOK()); EXPECT_EQ(status.GetErrorCode(), expected_code); EXPECT_THAT(status.GetErrorMessage(), ::testing::HasSubstr(expected_message)); From 25e9a5fb146a66f7a995fbe4c2f82cbc1b4ab19d Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Fri, 19 Jun 2026 17:04:31 -0700 Subject: [PATCH 32/48] Address review: validate Win32 UTF-8 conversions and guard null write-callback buffers --- .../test/autoep/ep_context_data_callbacks.h | 4 ++++ .../test/autoep/library/ep_context_data_utils.h | 14 ++++++++++---- .../test/framework/ep_plugin_provider_test.cc | 4 ++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/onnxruntime/test/autoep/ep_context_data_callbacks.h b/onnxruntime/test/autoep/ep_context_data_callbacks.h index 49472bc6dcb50..d25c7a5c571ee 100644 --- a/onnxruntime/test/autoep/ep_context_data_callbacks.h +++ b/onnxruntime/test/autoep/ep_context_data_callbacks.h @@ -31,6 +31,10 @@ inline OrtStatus* ORT_API_CALL StoreEpContextDataCallback(void* state, const cha callback_state->write_file_name = file_name; callback_state->payload.clear(); if (buffer_size != 0) { + if (buffer == nullptr) { + return Ort::GetApi().CreateStatus(ORT_INVALID_ARGUMENT, + "StoreEpContextDataCallback received a null buffer for non-empty data"); + } callback_state->payload.assign(static_cast(buffer), static_cast(buffer) + buffer_size); } return nullptr; diff --git a/onnxruntime/test/autoep/library/ep_context_data_utils.h b/onnxruntime/test/autoep/library/ep_context_data_utils.h index 23633d77afd21..e1de0a872022a 100644 --- a/onnxruntime/test/autoep/library/ep_context_data_utils.h +++ b/onnxruntime/test/autoep/library/ep_context_data_utils.h @@ -51,8 +51,11 @@ inline std::wstring Utf8ToWideString(std::string_view value) { } std::wstring wide_value(static_cast(wide_length), L'\0'); - MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), static_cast(value.size()), - wide_value.data(), wide_length); + const int converted = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), + static_cast(value.size()), wide_value.data(), wide_length); + if (converted != wide_length) { + return {}; + } return wide_value; } @@ -68,8 +71,11 @@ inline std::string WideToUtf8String(std::wstring_view value) { } std::string utf8_value(static_cast(utf8_length), '\0'); - WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast(value.size()), utf8_value.data(), utf8_length, - nullptr, nullptr); + const int converted = WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast(value.size()), + utf8_value.data(), utf8_length, nullptr, nullptr); + if (converted != utf8_length) { + return {}; + } return utf8_value; } #endif diff --git a/onnxruntime/test/framework/ep_plugin_provider_test.cc b/onnxruntime/test/framework/ep_plugin_provider_test.cc index ab613e3c2f3a1..797cac013f944 100644 --- a/onnxruntime/test/framework/ep_plugin_provider_test.cc +++ b/onnxruntime/test/framework/ep_plugin_provider_test.cc @@ -114,6 +114,10 @@ static OrtStatus* ORT_API_CALL EpContextWriteCallback(void* state, const char* f write_state->file_name = file_name; write_state->payload.clear(); if (buffer_size != 0) { + if (buffer == nullptr) { + return Ort::GetApi().CreateStatus(ORT_INVALID_ARGUMENT, + "EpContextWriteCallback received a null buffer for non-empty data"); + } write_state->payload.assign(static_cast(buffer), static_cast(buffer) + buffer_size); } return nullptr; From 70d850b5339ad55950b96555ea7ff5422faad8c6 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Fri, 19 Jun 2026 17:12:35 -0700 Subject: [PATCH 33/48] Add null-status guard to ExpectOrtStatusError test helper for consistency --- onnxruntime/test/autoep/ep_context_data_utils_test.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/onnxruntime/test/autoep/ep_context_data_utils_test.cc b/onnxruntime/test/autoep/ep_context_data_utils_test.cc index 50daa545f5c8d..f9b5936d2849f 100644 --- a/onnxruntime/test/autoep/ep_context_data_utils_test.cc +++ b/onnxruntime/test/autoep/ep_context_data_utils_test.cc @@ -39,6 +39,7 @@ OrtStatus* ORT_API_CALL LoadInvalidEpContextDataCallback(void* state, const char void ExpectOrtStatusError(OrtStatus* status_ptr, OrtErrorCode expected_code, std::string_view expected_message) { Ort::Status status{status_ptr}; + ASSERT_NE(status_ptr, nullptr) << "Expected a failure status, but the API returned nullptr (OK)."; ASSERT_FALSE(status.IsOK()); EXPECT_EQ(status.GetErrorCode(), expected_code); EXPECT_THAT(std::string{status.GetErrorMessage()}, ::testing::HasSubstr(std::string{expected_message})); From d54cf1c8ae23691fca7cc91caf78dfbe40a1f91d Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Mon, 22 Jun 2026 13:36:01 -0700 Subject: [PATCH 34/48] Clarify GetEpContextConfig doc: handle is non-NULL on success, *config unmodified on failure --- .../core/session/onnxruntime_experimental_c_api.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc b/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc index cf5b0d7c7b844..bde54b6167955 100644 --- a/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc +++ b/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc @@ -336,8 +336,8 @@ ORT_EXPERIMENTAL_API(28, OrtStatusPtr, OrtCompileApi_ModelCompilationOptions_Set /** \brief Extracts the EPContext configuration (callbacks and state) from an OrtSessionOptions instance. * * The EP should call this during CreateEp() while session_options is still valid, and store the returned handle for - * use during Compile(). The returned config is always non-NULL and must be released with - * OrtEpApi_ReleaseEpContextConfig. + * use during Compile(). On success, `*config` is set to a non-NULL handle that must be released with + * OrtEpApi_ReleaseEpContextConfig. On failure, an error status is returned and `*config` is not modified. * * The returned handle owns only ORT's copy of callback function pointers and opaque state pointer values. It does not * own the application-provided state. The application is responsible for keeping callback state valid and From 773f2fefde89f430715cf6a9fbec893efcb15f8c Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Mon, 22 Jun 2026 15:34:02 -0700 Subject: [PATCH 35/48] Address review: harden untrusted EPContext path resolution and merge resolvers Use weakly_canonical + base-directory containment for model-relative (untrusted) names so symlinks are followed and benign '..' segments are accepted; keep the lexical traversal guard for trusted (graph==null) paths and logical names. Merge ResolveEpContextDataOutputPath into ResolveEpContextDataPath. Use operator bool instead of bad() for the post-read stream check. Document the getter-taking *WithFileFallback overloads as a unit-test injection seam. --- .../autoep/library/ep_context_data_utils.h | 99 ++++++++++--------- 1 file changed, 53 insertions(+), 46 deletions(-) diff --git a/onnxruntime/test/autoep/library/ep_context_data_utils.h b/onnxruntime/test/autoep/library/ep_context_data_utils.h index a5bf7df84f3dd..3386a84c74856 100644 --- a/onnxruntime/test/autoep/library/ep_context_data_utils.h +++ b/onnxruntime/test/autoep/library/ep_context_data_utils.h @@ -100,6 +100,10 @@ inline std::string PathToUtf8String(const std::filesystem::path& path) { #endif } +// Lexical check for a ".." component. This is a coarse guard used for logical (callback-namespace) names and for +// trusted (graph == nullptr) paths. It is NOT a containment mechanism: it does not resolve symlinks and it rejects +// benign cases such as "a/b/c/../file.txt". Filesystem containment against a base directory is done by +// IsResolvedPathWithinBase() below, which the untrusted (model-relative) resolution path uses. inline bool ContainsPathTraversal(const std::filesystem::path& path) { const std::filesystem::path parent_dir{".."}; for (const auto& component : path) { @@ -114,6 +118,26 @@ inline bool HasAbsoluteOrRootedPath(const std::filesystem::path& path) { return path.is_absolute() || path.has_root_name() || path.has_root_directory(); } +// Returns true if `candidate_full` (a base-relative name already combined with `base`) resolves to a location inside +// `base`. Both are normalized with std::filesystem::weakly_canonical, which resolves "." / ".." and any symlinks in +// the existing portion of the path, so a name that escapes `base` directly or through a symlink is rejected. On +// success the canonical resolved path is written to `resolved`. +inline bool IsResolvedPathWithinBase(const std::filesystem::path& base, const std::filesystem::path& candidate_full, + std::filesystem::path& resolved) { + std::error_code ec; + const std::filesystem::path base_for_canon = base.empty() ? std::filesystem::path{"."} : base; + const std::filesystem::path canonical_base = std::filesystem::weakly_canonical(base_for_canon, ec); + if (ec) { + return false; + } + resolved = std::filesystem::weakly_canonical(candidate_full, ec); + if (ec) { + return false; + } + const std::filesystem::path relative = resolved.lexically_relative(canonical_base); + return !relative.empty() && *relative.begin() != std::filesystem::path{".."}; +} + inline OrtStatus* ValidateEpContextDataName(const OrtApi& api, const char* file_name, std::filesystem::path& data_name) { data_name.clear(); @@ -139,8 +163,14 @@ inline OrtStatus* ValidateEpContextDataName(const OrtApi& api, const char* file_ return nullptr; } -// Resolve file_name (which can originate from an untrusted EPContext model's "ep_cache_context" attribute) as a -// model-relative path when graph is available. Trusted direct callers may pass an absolute path when graph is null. +// Resolves `file_name` to a filesystem path for reading or writing EPContext data (used by both the read path and +// the write-fallback path). +// +// When `graph` is null the caller is trusted and owns the path: `file_name` is returned as-is and may be absolute (a +// lexical ".." is still rejected as a coarse guard). When `graph` is non-null, `file_name` originates from the +// untrusted EPContext model "ep_cache_context" attribute: it must be relative, and after combining it with the +// model's directory the result must stay within that directory. Symlinks and ".." are resolved (via +// weakly_canonical), so a name that escapes the model directory - including through a symlink - is rejected. // Production EPs should still apply their own sandboxing and size limits. inline OrtStatus* ResolveEpContextDataPath(const OrtApi& api, const char* file_name, const OrtGraph* graph, std::filesystem::path& data_path) { @@ -155,15 +185,16 @@ inline OrtStatus* ResolveEpContextDataPath(const OrtApi& api, const char* file_n return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name is not a valid path"); } - if (ContainsPathTraversal(candidate_path)) { - return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not contain path traversal"); - } - - data_path = candidate_path; + // Trusted direct callers (graph == nullptr) own the path and may pass an absolute physical path. if (graph == nullptr) { + if (ContainsPathTraversal(candidate_path)) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not contain path traversal"); + } + data_path = candidate_path; return nullptr; } + // Untrusted (model-derived) name: must be relative and must resolve within the model directory. if (HasAbsoluteOrRootedPath(candidate_path)) { return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute or rooted"); } @@ -171,48 +202,18 @@ inline OrtStatus* ResolveEpContextDataPath(const OrtApi& api, const char* file_n const ORTCHAR_T* model_path = nullptr; RETURN_IF_ERROR(api.Graph_GetModelPath(graph, &model_path)); if (model_path == nullptr || model_path[0] == 0) { + data_path = candidate_path; return nullptr; } - data_path = std::filesystem::path{model_path}.parent_path() / data_path; - return nullptr; -} - -inline OrtStatus* ResolveEpContextDataOutputPath(const OrtApi& api, const char* file_name, const OrtGraph* graph, - std::filesystem::path& data_path) { - data_path.clear(); - - if (file_name == nullptr || file_name[0] == '\0') { - return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); - } - - data_path = Utf8Path(file_name); - if (data_path.empty()) { - return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name is not a valid path"); + const std::filesystem::path base_dir = std::filesystem::path{model_path}.parent_path(); + std::filesystem::path resolved; + if (!IsResolvedPathWithinBase(base_dir, base_dir / candidate_path, resolved)) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, + "EPContext data file name must resolve to a path within the model directory"); } - if (ContainsPathTraversal(data_path)) { - return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not contain path traversal"); - } - - // Trusted direct callers (graph == nullptr) may supply an absolute physical path. When a graph is present, the - // name may be model-derived (untrusted), so reject absolute/rooted paths for symmetry with the read-side - // ResolveEpContextDataPath instead of writing to an attacker-chosen absolute or rooted location. - if (graph == nullptr) { - return nullptr; - } - - if (HasAbsoluteOrRootedPath(data_path)) { - return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute or rooted"); - } - - const ORTCHAR_T* model_path = nullptr; - RETURN_IF_ERROR(api.Graph_GetModelPath(graph, &model_path)); - if (model_path == nullptr || model_path[0] == 0) { - return nullptr; - } - - data_path = std::filesystem::path{model_path}.parent_path() / data_path; + data_path = resolved; return nullptr; } @@ -256,7 +257,7 @@ inline OrtStatus* ReadEpContextDataFromFile(const OrtApi& api, const char* file_ } data.assign(std::istreambuf_iterator{input_stream}, std::istreambuf_iterator{}); - if (input_stream.bad()) { + if (!input_stream) { const std::string message = "Failed to read EPContext data file: " + PathToUtf8String(data_path); return api.CreateStatus(ORT_FAIL, message.c_str()); @@ -276,6 +277,9 @@ inline OrtStatus* WriteEpContextDataToFile(const OrtApi& api, const char* file_n return WriteEpContextDataToResolvedFile(api, data_path, buffer, buffer_size); } +// Low-level overload that takes the GetEpContextDataReadFunc accessor explicitly. Production code should use the +// overload below that derives the accessor from `api`; this overload exists so unit tests can inject a fake accessor. +// `ep_context_config` is an opaque token only forwarded to `get_read_func` (never dereferenced here). inline OrtStatus* ReadEpContextDataWithFileFallback( const OrtApi& api, OrtExperimental_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn get_read_func, @@ -345,6 +349,9 @@ inline OrtStatus* ReadEpContextDataWithFileFallback( ep_context_config, file_name, graph, data); } +// Low-level overload that takes the GetEpContextDataWriteFunc accessor explicitly. Production code should use the +// overload below that derives the accessor from `api`; this overload exists so unit tests can inject a fake accessor. +// `ep_context_config` is an opaque token only forwarded to `get_write_func` (never dereferenced here). inline OrtStatus* WriteEpContextDataWithFileFallback( const OrtApi& api, OrtExperimental_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn get_write_func, @@ -384,7 +391,7 @@ inline OrtStatus* WriteEpContextDataWithFileFallback( } std::filesystem::path data_path; - RETURN_IF_ERROR(ResolveEpContextDataOutputPath(api, fallback_file_name, graph, data_path)); + RETURN_IF_ERROR(ResolveEpContextDataPath(api, fallback_file_name, graph, data_path)); return WriteEpContextDataToResolvedFile(api, data_path, buffer, buffer_size); } From e870307fb181058ef7485fe20dd81ec0773da6e5 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Mon, 22 Jun 2026 15:39:17 -0700 Subject: [PATCH 36/48] Address review: report UTF-8 path conversion errors via OrtStatus* Utf8ToWideString and Utf8Path now return OrtStatus* so an invalid-UTF-8 / unconvertible name produces a specific error instead of a silent empty string. The reverse helpers (WideToUtf8String/PathToUtf8String) stay best-effort since they only render already-valid paths into diagnostic messages. Updated the resolver/validator, the example EP, and the tests accordingly. --- .../test/autoep/ep_context_data_utils_test.cc | 20 ++++++--- .../autoep/library/ep_context_data_utils.h | 45 +++++++++++++------ .../autoep/library/example_plugin_ep/ep.cc | 10 +++-- onnxruntime/test/autoep/test_execution.cc | 6 ++- 4 files changed, 57 insertions(+), 24 deletions(-) diff --git a/onnxruntime/test/autoep/ep_context_data_utils_test.cc b/onnxruntime/test/autoep/ep_context_data_utils_test.cc index f1b2f90c4ee10..c10990eab15bc 100644 --- a/onnxruntime/test/autoep/ep_context_data_utils_test.cc +++ b/onnxruntime/test/autoep/ep_context_data_utils_test.cc @@ -89,22 +89,32 @@ Ort::Experimental::EpContextConfig MakeEmptyEpContextConfig() { } // namespace TEST(OrtEpLibrary, EpContextDataUtils_PathHelpersRoundTrip) { + const auto& api = Ort::GetApi(); const std::string file_name = "context_data.bin"; #ifdef _WIN32 - const std::wstring wide_file_name = ep_context_data_utils::Utf8ToWideString(file_name); + std::wstring wide_file_name; + ASSERT_ORTSTATUS_OK(ep_context_data_utils::Utf8ToWideString(api, file_name, wide_file_name)); ASSERT_FALSE(wide_file_name.empty()); EXPECT_EQ(ep_context_data_utils::WideToUtf8String(wide_file_name), file_name); const std::string invalid_utf8(1, static_cast(0xff)); - EXPECT_TRUE(ep_context_data_utils::Utf8ToWideString(invalid_utf8).empty()); + std::wstring invalid_wide; + ExpectOrtStatusError(ep_context_data_utils::Utf8ToWideString(api, invalid_utf8, invalid_wide), + ORT_INVALID_ARGUMENT, "not valid UTF-8"); + EXPECT_TRUE(invalid_wide.empty()); #endif - const std::filesystem::path file_path = ep_context_data_utils::Utf8Path(file_name.c_str()); + std::filesystem::path file_path; + ASSERT_ORTSTATUS_OK(ep_context_data_utils::Utf8Path(api, file_name.c_str(), file_path)); ASSERT_FALSE(file_path.empty()); EXPECT_EQ(ep_context_data_utils::PathToUtf8String(file_path), file_name); - EXPECT_TRUE(ep_context_data_utils::Utf8Path(nullptr).empty()); - EXPECT_TRUE(ep_context_data_utils::Utf8Path("").empty()); + + std::filesystem::path empty_path; + ASSERT_ORTSTATUS_OK(ep_context_data_utils::Utf8Path(api, nullptr, empty_path)); + EXPECT_TRUE(empty_path.empty()); + ASSERT_ORTSTATUS_OK(ep_context_data_utils::Utf8Path(api, "", empty_path)); + EXPECT_TRUE(empty_path.empty()); } TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathAndInvalidArguments) { diff --git a/onnxruntime/test/autoep/library/ep_context_data_utils.h b/onnxruntime/test/autoep/library/ep_context_data_utils.h index 3386a84c74856..d16236efe6fbf 100644 --- a/onnxruntime/test/autoep/library/ep_context_data_utils.h +++ b/onnxruntime/test/autoep/library/ep_context_data_utils.h @@ -39,26 +39,36 @@ namespace ep_context_data_utils { #ifdef _WIN32 -inline std::wstring Utf8ToWideString(std::string_view value) { - if (value.empty() || value.size() > static_cast(std::numeric_limits::max())) { - return {}; +// Converts a UTF-8 string to a wide string. Reports conversion failures (e.g., invalid UTF-8) via OrtStatus* instead +// of silently returning an empty string. An empty input yields an empty output and a success status. +inline OrtStatus* Utf8ToWideString(const OrtApi& api, std::string_view value, std::wstring& wide_value) { + wide_value.clear(); + if (value.empty()) { + return nullptr; + } + if (value.size() > static_cast(std::numeric_limits::max())) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name is too long to convert"); } const int wide_length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), static_cast(value.size()), nullptr, 0); if (wide_length <= 0) { - return {}; + return api.CreateStatus(ORT_INVALID_ARGUMENT, + "EPContext data file name is not valid UTF-8 or could not be converted to a wide string"); } - std::wstring wide_value(static_cast(wide_length), L'\0'); + wide_value.resize(static_cast(wide_length)); const int converted = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), static_cast(value.size()), wide_value.data(), wide_length); if (converted != wide_length) { - return {}; + wide_value.clear(); + return api.CreateStatus(ORT_FAIL, "Failed to convert EPContext data file name to a wide string"); } - return wide_value; + return nullptr; } +// Converts a wide string to UTF-8. This is used only to render already-valid paths into diagnostic messages, so it is +// intentionally best-effort: on the rare conversion failure it returns an empty string rather than an OrtStatus*. inline std::string WideToUtf8String(std::wstring_view value) { if (value.empty() || value.size() > static_cast(std::numeric_limits::max())) { return {}; @@ -80,16 +90,23 @@ inline std::string WideToUtf8String(std::wstring_view value) { } #endif -inline std::filesystem::path Utf8Path(const char* path) { +// Converts a UTF-8 path to a std::filesystem::path. A null or empty input yields an empty path and a success status; +// conversion failures are reported via OrtStatus*. +inline OrtStatus* Utf8Path(const OrtApi& api, const char* path, std::filesystem::path& out_path) { + out_path.clear(); if (path == nullptr || path[0] == '\0') { - return {}; + return nullptr; } #ifdef _WIN32 - return std::filesystem::path{Utf8ToWideString(path)}; + std::wstring wide_path; + RETURN_IF_ERROR(Utf8ToWideString(api, path, wide_path)); + out_path = std::filesystem::path{wide_path}; #else - return std::filesystem::path{path}; + (void)api; + out_path = std::filesystem::path{path}; #endif + return nullptr; } inline std::string PathToUtf8String(const std::filesystem::path& path) { @@ -146,7 +163,8 @@ inline OrtStatus* ValidateEpContextDataName(const OrtApi& api, const char* file_ return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); } - const auto candidate_path = Utf8Path(file_name); + std::filesystem::path candidate_path; + RETURN_IF_ERROR(Utf8Path(api, file_name, candidate_path)); if (candidate_path.empty()) { return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name is not a valid path"); } @@ -180,7 +198,8 @@ inline OrtStatus* ResolveEpContextDataPath(const OrtApi& api, const char* file_n return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); } - const auto candidate_path = Utf8Path(file_name); + std::filesystem::path candidate_path; + RETURN_IF_ERROR(Utf8Path(api, file_name, candidate_path)); if (candidate_path.empty()) { return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name is not a valid path"); } diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc index bc5e0d4e2661d..5a60dd8deebcb 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc @@ -542,12 +542,14 @@ OrtStatus* ExampleEp::CreateEpContextNodes(const OrtGraph* graph, std::string fallback_ep_ctx = ep_ctx; const OrtGraph* fallback_graph = graph; if (!config_.ep_context_output_model_path.empty()) { - const std::filesystem::path output_model_path = - ep_context_data_utils::Utf8Path(config_.ep_context_output_model_path.c_str()); + std::filesystem::path output_model_path; + RETURN_IF_ERROR(ep_context_data_utils::Utf8Path(ort_api, config_.ep_context_output_model_path.c_str(), + output_model_path)); const std::filesystem::path output_model_dir = output_model_path.parent_path(); if (!output_model_dir.empty()) { - fallback_ep_ctx = ep_context_data_utils::PathToUtf8String( - output_model_dir / ep_context_data_utils::Utf8Path(ep_ctx.c_str())); + std::filesystem::path ep_ctx_path; + RETURN_IF_ERROR(ep_context_data_utils::Utf8Path(ort_api, ep_ctx.c_str(), ep_ctx_path)); + fallback_ep_ctx = ep_context_data_utils::PathToUtf8String(output_model_dir / ep_ctx_path); } fallback_graph = nullptr; } diff --git a/onnxruntime/test/autoep/test_execution.cc b/onnxruntime/test/autoep/test_execution.cc index 84340f884ad3f..86e596d9700ba 100644 --- a/onnxruntime/test/autoep/test_execution.cc +++ b/onnxruntime/test/autoep/test_execution.cc @@ -688,8 +688,10 @@ TEST(OrtEpLibrary, PluginEp_GenAndLoadEpContextModel_ExternalDataUsesFileFallbac ASSERT_FALSE(ep_cache_context_attr->s().empty()); const std::filesystem::path output_model_dir = std::filesystem::path{output_model_file}.parent_path(); - const std::filesystem::path context_data_path = output_model_dir / - ep_context_data_utils::Utf8Path(ep_cache_context_attr->s().c_str()); + std::filesystem::path ep_cache_context_rel; + ASSERT_ORTSTATUS_OK( + ep_context_data_utils::Utf8Path(Ort::GetApi(), ep_cache_context_attr->s().c_str(), ep_cache_context_rel)); + const std::filesystem::path context_data_path = output_model_dir / ep_cache_context_rel; files_to_cleanup.push_back(context_data_path); ASSERT_TRUE(std::filesystem::exists(context_data_path)); From fa69850416c0683fd2ecf06be0da476f6ba856bb Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Mon, 22 Jun 2026 16:09:05 -0700 Subject: [PATCH 37/48] Address review findings in EPContext data path helper Reject model-derived file fallback when Graph_GetModelPath is empty so in-memory models cannot resolve untrusted relative paths without a base directory. Document why the write fallback validates the logical name even when the physical fallback path is separate, and add targeted unit tests for both behaviors. --- .../test/autoep/ep_context_data_utils_test.cc | 14 ++++++++++++++ .../test/autoep/library/ep_context_data_utils.h | 14 +++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/onnxruntime/test/autoep/ep_context_data_utils_test.cc b/onnxruntime/test/autoep/ep_context_data_utils_test.cc index c10990eab15bc..0c4f3c7e66858 100644 --- a/onnxruntime/test/autoep/ep_context_data_utils_test.cc +++ b/onnxruntime/test/autoep/ep_context_data_utils_test.cc @@ -12,6 +12,7 @@ #include #include +#include "core/graph/model_editor_api_types.h" #include "core/session/onnxruntime_cxx_api.h" #include "core/session/onnxruntime_experimental_cxx_api.h" @@ -183,6 +184,11 @@ TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathRejectsUnsafeNames) { ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback( api, nullptr, absolute_file_name, "unused.ctx", nullptr, nullptr, 0), ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute or rooted"); + + ModelEditorGraph empty_model_path_graph; + ExpectOrtStatusError(ep_context_data_utils::ResolveEpContextDataPath(api, "../escape.ctx", + empty_model_path_graph.ToExternal(), data_path), + ORT_INVALID_ARGUMENT, "requires a model path"); } TEST(OrtEpLibrary, EpContextDataUtils_FileFallbackReadsAndWrites) { @@ -217,6 +223,14 @@ TEST(OrtEpLibrary, EpContextDataUtils_FileFallbackReadsAndWrites) { api, nullptr, "logical_context_data.bin", fallback_data_file_name.c_str(), nullptr, payload.data(), payload.size())); + const std::filesystem::path unsafe_logical_fallback_path = test_dir / "unsafe_logical_context_data.bin"; + const std::string unsafe_logical_fallback_file_name = + ep_context_data_utils::PathToUtf8String(unsafe_logical_fallback_path); + ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback( + api, nullptr, "../logical_context_data.bin", + unsafe_logical_fallback_file_name.c_str(), nullptr, payload.data(), payload.size()), + ORT_INVALID_ARGUMENT, "EPContext data file name must not contain path traversal"); + data.clear(); ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataFromFile(api, fallback_data_file_name.c_str(), nullptr, data)); diff --git a/onnxruntime/test/autoep/library/ep_context_data_utils.h b/onnxruntime/test/autoep/library/ep_context_data_utils.h index d16236efe6fbf..d740bfab96336 100644 --- a/onnxruntime/test/autoep/library/ep_context_data_utils.h +++ b/onnxruntime/test/autoep/library/ep_context_data_utils.h @@ -186,9 +186,10 @@ inline OrtStatus* ValidateEpContextDataName(const OrtApi& api, const char* file_ // // When `graph` is null the caller is trusted and owns the path: `file_name` is returned as-is and may be absolute (a // lexical ".." is still rejected as a coarse guard). When `graph` is non-null, `file_name` originates from the -// untrusted EPContext model "ep_cache_context" attribute: it must be relative, and after combining it with the -// model's directory the result must stay within that directory. Symlinks and ".." are resolved (via -// weakly_canonical), so a name that escapes the model directory - including through a symlink - is rejected. +// untrusted EPContext model "ep_cache_context" attribute: the graph must have a model path, the name must be +// relative, and after combining it with the model's directory the result must stay within that directory. Symlinks and +// ".." are resolved (via weakly_canonical), so a name that escapes the model directory - including through a symlink - +// is rejected. // Production EPs should still apply their own sandboxing and size limits. inline OrtStatus* ResolveEpContextDataPath(const OrtApi& api, const char* file_name, const OrtGraph* graph, std::filesystem::path& data_path) { @@ -221,8 +222,8 @@ inline OrtStatus* ResolveEpContextDataPath(const OrtApi& api, const char* file_n const ORTCHAR_T* model_path = nullptr; RETURN_IF_ERROR(api.Graph_GetModelPath(graph, &model_path)); if (model_path == nullptr || model_path[0] == 0) { - data_path = candidate_path; - return nullptr; + return api.CreateStatus(ORT_INVALID_ARGUMENT, + "EPContext data file fallback requires a model path to resolve relative names"); } const std::filesystem::path base_dir = std::filesystem::path{model_path}.parent_path(); @@ -402,6 +403,9 @@ inline OrtStatus* WriteEpContextDataWithFileFallback( return write_func(write_state, file_name, buffer, buffer_size); } + // Even when the physical fallback path is supplied separately, `file_name` is the logical name written into the + // EPContext model's ep_cache_context attribute. Validate it as a safe relative name so a generated model cannot + // contain an unsafe logical reference that later reaches the read-side resolver. std::filesystem::path logical_path; RETURN_IF_ERROR(ValidateEpContextDataName(api, file_name, logical_path)); From b7055f7c7f0906f5c17b1ceaeeadcb8234d8bff0 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Mon, 22 Jun 2026 16:19:21 -0700 Subject: [PATCH 38/48] Add symlink escape coverage for EPContext data path resolver Add an optional EpContextDataUtils unit test that creates a directory symlink inside the model directory pointing outside and verifies model-derived resolution rejects the escaping path. The test skips cleanly when the platform/user lacks symlink creation privileges. --- .../test/autoep/ep_context_data_utils_test.cc | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/onnxruntime/test/autoep/ep_context_data_utils_test.cc b/onnxruntime/test/autoep/ep_context_data_utils_test.cc index 0c4f3c7e66858..5a79a439414ec 100644 --- a/onnxruntime/test/autoep/ep_context_data_utils_test.cc +++ b/onnxruntime/test/autoep/ep_context_data_utils_test.cc @@ -191,6 +191,33 @@ TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathRejectsUnsafeNames) { ORT_INVALID_ARGUMENT, "requires a model path"); } +TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathRejectsSymlinkEscape) { + const auto& api = Ort::GetApi(); + const std::filesystem::path test_dir = PrepareTempTestDir("ort_ep_context_data_utils_symlink_escape_test"); + auto cleanup = gsl::finally([&]() { std::filesystem::remove_all(test_dir); }); + + const std::filesystem::path model_dir = test_dir / "model_dir"; + const std::filesystem::path outside_dir = test_dir / "outside_dir"; + ASSERT_TRUE(std::filesystem::create_directories(model_dir)); + ASSERT_TRUE(std::filesystem::create_directories(outside_dir)); + + const std::filesystem::path symlink_path = model_dir / "linked_outside"; + std::error_code symlink_error; + std::filesystem::create_directory_symlink(outside_dir, symlink_path, symlink_error); + if (symlink_error) { + GTEST_SKIP() << "Unable to create directory symlink for containment test: " << symlink_error.message(); + } + + ModelEditorGraph graph; + graph.model_path = model_dir / "model.onnx"; + + std::filesystem::path data_path; + ExpectOrtStatusError(ep_context_data_utils::ResolveEpContextDataPath(api, "linked_outside/escape.ctx", + graph.ToExternal(), data_path), + ORT_INVALID_ARGUMENT, "resolve to a path within the model directory"); + EXPECT_TRUE(data_path.empty()); +} + TEST(OrtEpLibrary, EpContextDataUtils_FileFallbackReadsAndWrites) { const auto& api = Ort::GetApi(); const std::filesystem::path test_dir = PrepareTempTestDir("ort_ep_context_data_utils_file_fallback_test"); From b6203acdd3868a3f5287f521a458934addaf9484 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Mon, 22 Jun 2026 16:34:18 -0700 Subject: [PATCH 39/48] Clarify EPContext write callback interaction with binary info Document that the EPContext data write callback can coexist with ModelCompilationOptions_SetEpContextBinaryInformation: the binary information remains useful for output/model naming and file fallback, while the callback overrides actual EPContext data persistence. Add a framework test that configures both APIs and verifies the path config remains set and the write callback is still returned and usable. --- .../onnxruntime_experimental_c_api.inc | 5 ++ .../test/framework/ep_plugin_provider_test.cc | 48 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc b/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc index bde54b6167955..bcfc22becb152 100644 --- a/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc +++ b/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc @@ -311,6 +311,11 @@ ORT_EXPERIMENTAL_API(28, OrtStatusPtr, OrtApi_SessionOptions_SetEpContextDataRea * When EPContext embed mode is disabled, execution providers can retrieve this callback from OrtEpContextConfig and * call it instead of writing EPContext binary data directly to disk. * + * This callback may be used together with OrtApi::ModelCompilationOptions_SetEpContextBinaryInformation. The binary + * information still describes the compiled model/output location that EPs may use to generate stable logical + * EPContext data names or as a file-fallback location. If this callback is configured, EPs should call it for + * EPContext binary data instead of writing that data to the fallback file path. + * * Writing happens only at compile time, so this callback is configured on OrtModelCompilationOptions. The * corresponding read callback runs at session load and is configured on OrtSessionOptions via * OrtApi_SessionOptions_SetEpContextDataReadFunc. diff --git a/onnxruntime/test/framework/ep_plugin_provider_test.cc b/onnxruntime/test/framework/ep_plugin_provider_test.cc index be91393149215..240e619529004 100644 --- a/onnxruntime/test/framework/ep_plugin_provider_test.cc +++ b/onnxruntime/test/framework/ep_plugin_provider_test.cc @@ -2016,6 +2016,54 @@ TEST(PluginExecutionProviderTest, EpContextDataWriteFuncIsReturnedByEpApi) { EXPECT_EQ(callback_state.file_name, "engine.bin"); EXPECT_EQ(callback_state.payload, payload); } + +TEST(PluginExecutionProviderTest, EpContextDataWriteFuncCanBeUsedWithEpContextBinaryInformation) { + const auto& ort_api = Ort::GetApi(); + Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "EpContextDataWriteFuncCanBeUsedWithEpContextBinaryInformation"}; + Ort::SessionOptions session_options; + Ort::ModelCompilationOptions compilation_options{env, session_options}; + + auto set_write_func = + Ort::Experimental::Get_OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); + auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&ort_api); + auto release_config_func = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); + auto get_write_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); + ASSERT_NE(set_write_func, nullptr); + ASSERT_NE(get_config, nullptr); + ASSERT_NE(release_config_func, nullptr); + ASSERT_NE(get_write_func, nullptr); + + ASSERT_NO_THROW(compilation_options.SetEpContextBinaryInformation(ORT_TSTR("ep_context_dir/"), + ORT_TSTR("compiled_model.onnx"))); + + EpContextWriteCallbackState callback_state{}; + ASSERT_ORTSTATUS_OK(set_write_func(compilation_options, EpContextWriteCallback, &callback_state)); + + const auto* internal_options = reinterpret_cast( + static_cast(compilation_options)); + const auto& internal_session_options = internal_options->GetSessionOptions(); + + std::string ep_context_file_path; + ASSERT_TRUE(internal_session_options.value.config_options.TryGetConfigEntry(kOrtSessionOptionEpContextFilePath, + ep_context_file_path)); + EXPECT_THAT(ep_context_file_path, ::testing::HasSubstr("compiled_model.onnx")); + + OrtEpContextConfig* ep_context_config = nullptr; + ASSERT_ORTSTATUS_OK(get_config(&internal_session_options, &ep_context_config)); + auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); + + OrtWriteNamedBufferFunc write_func = nullptr; + void* callback_state_out = nullptr; + ASSERT_ORTSTATUS_OK(get_write_func(ep_context_config, &write_func, &callback_state_out)); + ASSERT_EQ(write_func, EpContextWriteCallback); + ASSERT_EQ(callback_state_out, &callback_state); + + const std::vector payload{'c', 't', 'x'}; + ASSERT_ORTSTATUS_OK(write_func(callback_state_out, "logical_context.bin", payload.data(), payload.size())); + EXPECT_TRUE(callback_state.called); + EXPECT_EQ(callback_state.file_name, "logical_context.bin"); + EXPECT_EQ(callback_state.payload, payload); +} #endif // !defined(ORT_MINIMAL_BUILD) TEST(PluginExecutionProviderTest, EpContextDataReturnedReadFuncAllowsEmptyPayloads) { From 77ffd697e380ea0b08cd885d17307e72f4c0a319 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Mon, 22 Jun 2026 17:19:28 -0700 Subject: [PATCH 40/48] Fix EpContextDataUtils symlink-escape test: use absolute symlink target A relative symlink target is resolved by the OS relative to the link's own directory, leaving the link dangling so weakly_canonical never traverses it and the containment check fails to detect the escape (CI-only, since the test skips on unprivileged dev boxes). Use an absolute target. --- onnxruntime/test/autoep/ep_context_data_utils_test.cc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/onnxruntime/test/autoep/ep_context_data_utils_test.cc b/onnxruntime/test/autoep/ep_context_data_utils_test.cc index 5a79a439414ec..2e2566d18b275 100644 --- a/onnxruntime/test/autoep/ep_context_data_utils_test.cc +++ b/onnxruntime/test/autoep/ep_context_data_utils_test.cc @@ -202,8 +202,13 @@ TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathRejectsSymlinkEscape) { ASSERT_TRUE(std::filesystem::create_directories(outside_dir)); const std::filesystem::path symlink_path = model_dir / "linked_outside"; + // The symlink target must be absolute. A relative target is stored verbatim and resolved by the OS relative to the + // link's own directory (model_dir), not the test's working directory, which would make "linked_outside" a dangling + // link. weakly_canonical() does not traverse a dangling symlink, so the containment check would treat + // "linked_outside/escape.ctx" as an ordinary (non-existent) child of model_dir and fail to detect the escape. + const std::filesystem::path symlink_target = std::filesystem::absolute(outside_dir); std::error_code symlink_error; - std::filesystem::create_directory_symlink(outside_dir, symlink_path, symlink_error); + std::filesystem::create_directory_symlink(symlink_target, symlink_path, symlink_error); if (symlink_error) { GTEST_SKIP() << "Unable to create directory symlink for containment test: " << symlink_error.message(); } From 39ed3610956312eb7680bc9e38408e2bed9c8ef4 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Tue, 23 Jun 2026 16:04:56 -0700 Subject: [PATCH 41/48] Address EPContext helper review feedback Align EpContextConfig with C++ wrapper constructor conventions, improve EPContext data path conversion diagnostics, keep containment output untouched on failure, and avoid exposing experimental getter typedefs from sample helper overloads. --- .../onnxruntime_experimental_cxx_api.h | 5 + .../test/autoep/ep_context_data_utils_test.cc | 32 ++++-- .../autoep/library/ep_context_data_utils.h | 99 +++++++++++++------ .../autoep/library/example_plugin_ep/ep.cc | 3 +- onnxruntime/test/autoep/test_execution.cc | 4 +- 5 files changed, 101 insertions(+), 42 deletions(-) diff --git a/include/onnxruntime/core/session/onnxruntime_experimental_cxx_api.h b/include/onnxruntime/core/session/onnxruntime_experimental_cxx_api.h index 0f42439916d62..bd6f86a1a4606 100644 --- a/include/onnxruntime/core/session/onnxruntime_experimental_cxx_api.h +++ b/include/onnxruntime/core/session/onnxruntime_experimental_cxx_api.h @@ -117,6 +117,11 @@ namespace Experimental { class EpContextConfig { public: EpContextConfig() noexcept = default; + explicit EpContextConfig(std::nullptr_t) noexcept {} + + explicit EpContextConfig(const SessionOptions& session_options) : EpContextConfig{session_options.GetConst()} {} + explicit EpContextConfig(ConstSessionOptions session_options) + : EpContextConfig{static_cast(session_options)} {} // Extracts the EPContext config from `session_options`. Throws Ort::Exception (ORT_NOT_IMPLEMENTED) if the // experimental functions are not available in this build, or propagates any error from the extraction. diff --git a/onnxruntime/test/autoep/ep_context_data_utils_test.cc b/onnxruntime/test/autoep/ep_context_data_utils_test.cc index 2e2566d18b275..767522d6a23ec 100644 --- a/onnxruntime/test/autoep/ep_context_data_utils_test.cc +++ b/onnxruntime/test/autoep/ep_context_data_utils_test.cc @@ -97,7 +97,9 @@ TEST(OrtEpLibrary, EpContextDataUtils_PathHelpersRoundTrip) { std::wstring wide_file_name; ASSERT_ORTSTATUS_OK(ep_context_data_utils::Utf8ToWideString(api, file_name, wide_file_name)); ASSERT_FALSE(wide_file_name.empty()); - EXPECT_EQ(ep_context_data_utils::WideToUtf8String(wide_file_name), file_name); + std::string round_tripped_file_name; + ASSERT_ORTSTATUS_OK(ep_context_data_utils::WideToUtf8String(api, wide_file_name, round_tripped_file_name)); + EXPECT_EQ(round_tripped_file_name, file_name); const std::string invalid_utf8(1, static_cast(0xff)); std::wstring invalid_wide; @@ -109,7 +111,9 @@ TEST(OrtEpLibrary, EpContextDataUtils_PathHelpersRoundTrip) { std::filesystem::path file_path; ASSERT_ORTSTATUS_OK(ep_context_data_utils::Utf8Path(api, file_name.c_str(), file_path)); ASSERT_FALSE(file_path.empty()); - EXPECT_EQ(ep_context_data_utils::PathToUtf8String(file_path), file_name); + std::string round_tripped_path; + ASSERT_ORTSTATUS_OK(ep_context_data_utils::PathToUtf8String(api, file_path, round_tripped_path)); + EXPECT_EQ(round_tripped_path, file_name); std::filesystem::path empty_path; ASSERT_ORTSTATUS_OK(ep_context_data_utils::Utf8Path(api, nullptr, empty_path)); @@ -133,7 +137,9 @@ TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathAndInvalidArguments) { EXPECT_TRUE(data_path.empty()); ASSERT_ORTSTATUS_OK(ep_context_data_utils::ResolveEpContextDataPath(api, "relative.ctx", nullptr, data_path)); - EXPECT_EQ(ep_context_data_utils::PathToUtf8String(data_path), "relative.ctx"); + std::string resolved_data_path; + ASSERT_ORTSTATUS_OK(ep_context_data_utils::PathToUtf8String(api, data_path, resolved_data_path)); + EXPECT_EQ(resolved_data_path, "relative.ctx"); ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataToFile(api, "unused.ctx", nullptr, nullptr, 1), ORT_INVALID_ARGUMENT, "EPContext data buffer must not be null for non-empty data"); @@ -230,7 +236,8 @@ TEST(OrtEpLibrary, EpContextDataUtils_FileFallbackReadsAndWrites) { const std::string payload = "file fallback payload"; const std::filesystem::path data_path = test_dir / "context_data.bin"; - const std::string data_file_name = ep_context_data_utils::PathToUtf8String(data_path); + std::string data_file_name; + ASSERT_ORTSTATUS_OK(ep_context_data_utils::PathToUtf8String(api, data_path, data_file_name)); ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataToFile(api, data_file_name.c_str(), nullptr, payload.data(), payload.size())); @@ -240,7 +247,8 @@ TEST(OrtEpLibrary, EpContextDataUtils_FileFallbackReadsAndWrites) { EXPECT_EQ(std::string(data.begin(), data.end()), payload); const std::filesystem::path wrapper_data_path = test_dir / "wrapper_context_data.bin"; - const std::string wrapper_data_file_name = ep_context_data_utils::PathToUtf8String(wrapper_data_path); + std::string wrapper_data_file_name; + ASSERT_ORTSTATUS_OK(ep_context_data_utils::PathToUtf8String(api, wrapper_data_path, wrapper_data_file_name)); ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( api, nullptr, wrapper_data_file_name.c_str(), nullptr, payload.data(), payload.size())); @@ -250,14 +258,16 @@ TEST(OrtEpLibrary, EpContextDataUtils_FileFallbackReadsAndWrites) { EXPECT_EQ(std::string(data.begin(), data.end()), payload); const std::filesystem::path fallback_data_path = test_dir / "fallback_context_data.bin"; - const std::string fallback_data_file_name = ep_context_data_utils::PathToUtf8String(fallback_data_path); + std::string fallback_data_file_name; + ASSERT_ORTSTATUS_OK(ep_context_data_utils::PathToUtf8String(api, fallback_data_path, fallback_data_file_name)); ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( api, nullptr, "logical_context_data.bin", fallback_data_file_name.c_str(), nullptr, payload.data(), payload.size())); const std::filesystem::path unsafe_logical_fallback_path = test_dir / "unsafe_logical_context_data.bin"; - const std::string unsafe_logical_fallback_file_name = - ep_context_data_utils::PathToUtf8String(unsafe_logical_fallback_path); + std::string unsafe_logical_fallback_file_name; + ASSERT_ORTSTATUS_OK(ep_context_data_utils::PathToUtf8String(api, unsafe_logical_fallback_path, + unsafe_logical_fallback_file_name)); ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback( api, nullptr, "../logical_context_data.bin", unsafe_logical_fallback_file_name.c_str(), nullptr, payload.data(), payload.size()), @@ -269,7 +279,8 @@ TEST(OrtEpLibrary, EpContextDataUtils_FileFallbackReadsAndWrites) { EXPECT_EQ(std::string(data.begin(), data.end()), payload); const std::filesystem::path empty_data_path = test_dir / "empty_context_data.bin"; - const std::string empty_data_file_name = ep_context_data_utils::PathToUtf8String(empty_data_path); + std::string empty_data_file_name; + ASSERT_ORTSTATUS_OK(ep_context_data_utils::PathToUtf8String(api, empty_data_path, empty_data_file_name)); ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( api, nullptr, empty_data_file_name.c_str(), nullptr, nullptr, 0)); @@ -279,7 +290,8 @@ TEST(OrtEpLibrary, EpContextDataUtils_FileFallbackReadsAndWrites) { EXPECT_TRUE(data.empty()); const std::filesystem::path missing_data_path = test_dir / "missing_context_data.bin"; - const std::string missing_data_file_name = ep_context_data_utils::PathToUtf8String(missing_data_path); + std::string missing_data_file_name; + ASSERT_ORTSTATUS_OK(ep_context_data_utils::PathToUtf8String(api, missing_data_path, missing_data_file_name)); ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataFromFile(api, missing_data_file_name.c_str(), nullptr, data), ORT_FAIL, "Failed to open EPContext data file for read"); diff --git a/onnxruntime/test/autoep/library/ep_context_data_utils.h b/onnxruntime/test/autoep/library/ep_context_data_utils.h index d740bfab96336..33ce0ac0cf5da 100644 --- a/onnxruntime/test/autoep/library/ep_context_data_utils.h +++ b/onnxruntime/test/autoep/library/ep_context_data_utils.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #ifdef _WIN32 @@ -38,7 +39,16 @@ // size limits, and path policies; see the per-function notes on how untrusted, model-derived names are treated. namespace ep_context_data_utils { +using GetEpContextDataReadFunc = OrtStatus*(ORT_API_CALL*)(const OrtEpContextConfig* config, + OrtReadNamedBufferFunc* read_func, void** state); +using GetEpContextDataWriteFunc = OrtStatus*(ORT_API_CALL*)(const OrtEpContextConfig* config, + OrtWriteNamedBufferFunc* write_func, void** state); + #ifdef _WIN32 +inline std::string WindowsLastErrorMessage(std::string_view message, DWORD error_code) { + return std::string{message} + " GetLastError=" + std::to_string(error_code); +} + // Converts a UTF-8 string to a wide string. Reports conversion failures (e.g., invalid UTF-8) via OrtStatus* instead // of silently returning an empty string. An empty input yields an empty output and a success status. inline OrtStatus* Utf8ToWideString(const OrtApi& api, std::string_view value, std::wstring& wide_value) { @@ -53,8 +63,9 @@ inline OrtStatus* Utf8ToWideString(const OrtApi& api, std::string_view value, st const int wide_length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), static_cast(value.size()), nullptr, 0); if (wide_length <= 0) { - return api.CreateStatus(ORT_INVALID_ARGUMENT, - "EPContext data file name is not valid UTF-8 or could not be converted to a wide string"); + const std::string message = WindowsLastErrorMessage( + "EPContext data file name is not valid UTF-8 or could not be converted to a wide string.", GetLastError()); + return api.CreateStatus(ORT_INVALID_ARGUMENT, message.c_str()); } wide_value.resize(static_cast(wide_length)); @@ -62,31 +73,42 @@ inline OrtStatus* Utf8ToWideString(const OrtApi& api, std::string_view value, st static_cast(value.size()), wide_value.data(), wide_length); if (converted != wide_length) { wide_value.clear(); - return api.CreateStatus(ORT_FAIL, "Failed to convert EPContext data file name to a wide string"); + const std::string message = WindowsLastErrorMessage("Failed to convert EPContext data file name to a wide string.", + GetLastError()); + return api.CreateStatus(ORT_FAIL, message.c_str()); } return nullptr; } -// Converts a wide string to UTF-8. This is used only to render already-valid paths into diagnostic messages, so it is -// intentionally best-effort: on the rare conversion failure it returns an empty string rather than an OrtStatus*. -inline std::string WideToUtf8String(std::wstring_view value) { - if (value.empty() || value.size() > static_cast(std::numeric_limits::max())) { - return {}; +// Converts a wide string to UTF-8. Reports conversion failures via OrtStatus* instead of silently returning an empty +// string. An empty input yields an empty output and a success status. +inline OrtStatus* WideToUtf8String(const OrtApi& api, std::wstring_view value, std::string& utf8_value) { + utf8_value.clear(); + if (value.empty()) { + return nullptr; + } + if (value.size() > static_cast(std::numeric_limits::max())) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name is too long to convert"); } const int utf8_length = WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast(value.size()), nullptr, 0, nullptr, nullptr); if (utf8_length <= 0) { - return {}; + const std::string message = WindowsLastErrorMessage( + "EPContext data file name could not be converted to UTF-8.", GetLastError()); + return api.CreateStatus(ORT_INVALID_ARGUMENT, message.c_str()); } - std::string utf8_value(static_cast(utf8_length), '\0'); + utf8_value.resize(static_cast(utf8_length)); const int converted = WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast(value.size()), utf8_value.data(), utf8_length, nullptr, nullptr); if (converted != utf8_length) { - return {}; + utf8_value.clear(); + const std::string message = WindowsLastErrorMessage("Failed to convert EPContext data file name to UTF-8.", + GetLastError()); + return api.CreateStatus(ORT_FAIL, message.c_str()); } - return utf8_value; + return nullptr; } #endif @@ -109,18 +131,28 @@ inline OrtStatus* Utf8Path(const OrtApi& api, const char* path, std::filesystem: return nullptr; } -inline std::string PathToUtf8String(const std::filesystem::path& path) { +inline OrtStatus* PathToUtf8String(const OrtApi& api, const std::filesystem::path& path, std::string& utf8_path) { + utf8_path.clear(); #ifdef _WIN32 - return WideToUtf8String(path.wstring()); + RETURN_IF_ERROR(WideToUtf8String(api, path.wstring(), utf8_path)); #else - return path.string(); + (void)api; + utf8_path = path.string(); #endif + return nullptr; } -// Lexical check for a ".." component. This is a coarse guard used for logical (callback-namespace) names and for -// trusted (graph == nullptr) paths. It is NOT a containment mechanism: it does not resolve symlinks and it rejects -// benign cases such as "a/b/c/../file.txt". Filesystem containment against a base directory is done by -// IsResolvedPathWithinBase() below, which the untrusted (model-relative) resolution path uses. +inline std::string PathToUtf8StringForMessage(const std::filesystem::path& path) { + std::string utf8_path; + Ort::Status status{PathToUtf8String(Ort::GetApi(), path, utf8_path)}; + return status.IsOK() ? utf8_path : std::string{""}; +} + +// Lexical check for a ".." component. This is a coarse guard used when there is no filesystem base directory to +// contain against: logical callback-namespace names and trusted (graph == nullptr) physical paths. It is NOT a +// containment mechanism: it does not resolve symlinks and it rejects benign cases such as "a/b/c/../file.txt". +// Filesystem containment against a model directory is done by IsResolvedPathWithinBase() below, which the untrusted +// (model-relative) resolution path uses. inline bool ContainsPathTraversal(const std::filesystem::path& path) { const std::filesystem::path parent_dir{".."}; for (const auto& component : path) { @@ -147,12 +179,17 @@ inline bool IsResolvedPathWithinBase(const std::filesystem::path& base, const st if (ec) { return false; } - resolved = std::filesystem::weakly_canonical(candidate_full, ec); + std::filesystem::path candidate_resolved = std::filesystem::weakly_canonical(candidate_full, ec); if (ec) { return false; } - const std::filesystem::path relative = resolved.lexically_relative(canonical_base); - return !relative.empty() && *relative.begin() != std::filesystem::path{".."}; + const std::filesystem::path relative = candidate_resolved.lexically_relative(canonical_base); + if (relative.empty() || *relative.begin() == std::filesystem::path{".."}) { + return false; + } + + resolved = std::move(candidate_resolved); + return true; } inline OrtStatus* ValidateEpContextDataName(const OrtApi& api, const char* file_name, @@ -190,7 +227,9 @@ inline OrtStatus* ValidateEpContextDataName(const OrtApi& api, const char* file_ // relative, and after combining it with the model's directory the result must stay within that directory. Symlinks and // ".." are resolved (via weakly_canonical), so a name that escapes the model directory - including through a symlink - // is rejected. -// Production EPs should still apply their own sandboxing and size limits. +// This helper only decides whether a model-derived file name resolves inside the model directory. Production EPs +// should still choose an application-approved storage root (sandbox), reject special files/locations as appropriate, +// and cap the number of bytes they will read or write for a single EPContext payload. inline OrtStatus* ResolveEpContextDataPath(const OrtApi& api, const char* file_name, const OrtGraph* graph, std::filesystem::path& data_path) { data_path.clear(); @@ -242,7 +281,7 @@ inline OrtStatus* WriteEpContextDataToResolvedFile(const OrtApi& api, const std: std::ofstream output_stream(data_path, std::ios::binary); if (!output_stream) { const std::string message = "Failed to open EPContext data file for write: " + - PathToUtf8String(data_path); + PathToUtf8StringForMessage(data_path); return api.CreateStatus(ORT_FAIL, message.c_str()); } @@ -254,7 +293,7 @@ inline OrtStatus* WriteEpContextDataToResolvedFile(const OrtApi& api, const std: output_stream.write(static_cast(buffer), static_cast(buffer_size)); if (!output_stream) { const std::string message = "Failed to write EPContext data file: " + - PathToUtf8String(data_path); + PathToUtf8StringForMessage(data_path); return api.CreateStatus(ORT_FAIL, message.c_str()); } } @@ -272,14 +311,14 @@ inline OrtStatus* ReadEpContextDataFromFile(const OrtApi& api, const char* file_ std::ifstream input_stream(data_path, std::ios::binary); if (!input_stream) { const std::string message = "Failed to open EPContext data file for read: " + - PathToUtf8String(data_path); + PathToUtf8StringForMessage(data_path); return api.CreateStatus(ORT_FAIL, message.c_str()); } data.assign(std::istreambuf_iterator{input_stream}, std::istreambuf_iterator{}); if (!input_stream) { const std::string message = "Failed to read EPContext data file: " + - PathToUtf8String(data_path); + PathToUtf8StringForMessage(data_path); return api.CreateStatus(ORT_FAIL, message.c_str()); } @@ -302,7 +341,7 @@ inline OrtStatus* WriteEpContextDataToFile(const OrtApi& api, const char* file_n // `ep_context_config` is an opaque token only forwarded to `get_read_func` (never dereferenced here). inline OrtStatus* ReadEpContextDataWithFileFallback( const OrtApi& api, - OrtExperimental_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn get_read_func, + GetEpContextDataReadFunc get_read_func, const OrtEpContextConfig* ep_context_config, const char* file_name, const OrtGraph* graph, std::vector& data) { @@ -374,7 +413,7 @@ inline OrtStatus* ReadEpContextDataWithFileFallback( // `ep_context_config` is an opaque token only forwarded to `get_write_func` (never dereferenced here). inline OrtStatus* WriteEpContextDataWithFileFallback( const OrtApi& api, - OrtExperimental_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn get_write_func, + GetEpContextDataWriteFunc get_write_func, const OrtEpContextConfig* ep_context_config, const char* file_name, const char* fallback_file_name, const OrtGraph* graph, @@ -437,7 +476,7 @@ inline OrtStatus* WriteEpContextDataWithFileFallback( inline OrtStatus* WriteEpContextDataWithFileFallback( const OrtApi& api, - OrtExperimental_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn get_write_func, + GetEpContextDataWriteFunc get_write_func, const OrtEpContextConfig* ep_context_config, const char* file_name, const OrtGraph* graph, const void* buffer, size_t buffer_size) { diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc index 5a60dd8deebcb..71f2dfac67d08 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc @@ -549,7 +549,8 @@ OrtStatus* ExampleEp::CreateEpContextNodes(const OrtGraph* graph, if (!output_model_dir.empty()) { std::filesystem::path ep_ctx_path; RETURN_IF_ERROR(ep_context_data_utils::Utf8Path(ort_api, ep_ctx.c_str(), ep_ctx_path)); - fallback_ep_ctx = ep_context_data_utils::PathToUtf8String(output_model_dir / ep_ctx_path); + RETURN_IF_ERROR(ep_context_data_utils::PathToUtf8String(ort_api, output_model_dir / ep_ctx_path, + fallback_ep_ctx)); } fallback_graph = nullptr; } diff --git a/onnxruntime/test/autoep/test_execution.cc b/onnxruntime/test/autoep/test_execution.cc index 86e596d9700ba..fa4d7cbf8c8ca 100644 --- a/onnxruntime/test/autoep/test_execution.cc +++ b/onnxruntime/test/autoep/test_execution.cc @@ -696,7 +696,9 @@ TEST(OrtEpLibrary, PluginEp_GenAndLoadEpContextModel_ExternalDataUsesFileFallbac ASSERT_TRUE(std::filesystem::exists(context_data_path)); std::vector context_data; - const std::string context_data_file_name = ep_context_data_utils::PathToUtf8String(context_data_path); + std::string context_data_file_name; + ASSERT_ORTSTATUS_OK(ep_context_data_utils::PathToUtf8String(Ort::GetApi(), context_data_path, + context_data_file_name)); ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataFromFile(Ort::GetApi(), context_data_file_name.c_str(), nullptr, context_data)); EXPECT_EQ(std::string(context_data.begin(), context_data.end()), "binary_data"); From 88e2e3ae9d157b66ba865b9f1295d4d51d8b26be Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Wed, 24 Jun 2026 10:50:07 -0700 Subject: [PATCH 42/48] Address latest EPContext review comments Move public EPContext data API tests to the shared_lib test target, keep framework tests focused on internal write-callback plumbing, use required experimental accessors via FnOrThrow, make the symlink test exercise a valid relative link target, and wrap helper calls with ASSERT_NO_FATAL_FAILURE. --- cmake/onnxruntime_unittests.cmake | 1 + .../test/autoep/ep_context_data_utils_test.cc | 9 +- onnxruntime/test/autoep/test_execution.cc | 26 +- .../test/framework/ep_plugin_provider_test.cc | 306 +---------------- .../shared_lib/test_ep_context_data_api.cc | 323 ++++++++++++++++++ 5 files changed, 360 insertions(+), 305 deletions(-) create mode 100644 onnxruntime/test/shared_lib/test_ep_context_data_api.cc diff --git a/cmake/onnxruntime_unittests.cmake b/cmake/onnxruntime_unittests.cmake index d95bf0c4c6189..15b4cd8c8bcdf 100644 --- a/cmake/onnxruntime_unittests.cmake +++ b/cmake/onnxruntime_unittests.cmake @@ -603,6 +603,7 @@ set (onnxruntime_shared_lib_test_SRC ${ONNXRUNTIME_SHARED_LIB_TEST_SRC_DIR}/custom_op_utils.cc ${ONNXRUNTIME_SHARED_LIB_TEST_SRC_DIR}/test_allocator.cc ${ONNXRUNTIME_SHARED_LIB_TEST_SRC_DIR}/test_data_copy.cc + ${ONNXRUNTIME_SHARED_LIB_TEST_SRC_DIR}/test_ep_context_data_api.cc ${ONNXRUNTIME_SHARED_LIB_TEST_SRC_DIR}/test_experimental_api.cc ${ONNXRUNTIME_SHARED_LIB_TEST_SRC_DIR}/test_fixture.h ${ONNXRUNTIME_SHARED_LIB_TEST_SRC_DIR}/test_model_loading.cc diff --git a/onnxruntime/test/autoep/ep_context_data_utils_test.cc b/onnxruntime/test/autoep/ep_context_data_utils_test.cc index 767522d6a23ec..1bd17d6801835 100644 --- a/onnxruntime/test/autoep/ep_context_data_utils_test.cc +++ b/onnxruntime/test/autoep/ep_context_data_utils_test.cc @@ -208,11 +208,10 @@ TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathRejectsSymlinkEscape) { ASSERT_TRUE(std::filesystem::create_directories(outside_dir)); const std::filesystem::path symlink_path = model_dir / "linked_outside"; - // The symlink target must be absolute. A relative target is stored verbatim and resolved by the OS relative to the - // link's own directory (model_dir), not the test's working directory, which would make "linked_outside" a dangling - // link. weakly_canonical() does not traverse a dangling symlink, so the containment check would treat - // "linked_outside/escape.ctx" as an ordinary (non-existent) child of model_dir and fail to detect the escape. - const std::filesystem::path symlink_target = std::filesystem::absolute(outside_dir); + // Relative symlink targets are resolved by the OS relative to the link's own directory, not the test's working + // directory. Point to the sibling outside_dir using a link-relative target; using the test_dir-relative + // `outside_dir` path here would create a dangling link under model_dir, and weakly_canonical() would not traverse it. + const std::filesystem::path symlink_target = std::filesystem::path{".."} / outside_dir.filename(); std::error_code symlink_error; std::filesystem::create_directory_symlink(symlink_target, symlink_path, symlink_error); if (symlink_error) { diff --git a/onnxruntime/test/autoep/test_execution.cc b/onnxruntime/test/autoep/test_execution.cc index fa4d7cbf8c8ca..e95918c719324 100644 --- a/onnxruntime/test/autoep/test_execution.cc +++ b/onnxruntime/test/autoep/test_execution.cc @@ -36,19 +36,17 @@ namespace { // Invokes the experimental EPContext read setter on the public C API. void SetEpContextDataReadFunc(Ort::SessionOptions& session_options, OrtReadNamedBufferFunc read_func, void* state) { - auto set_read_func = - Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_Fn(&Ort::GetApi()); - ASSERT_NE(set_read_func, nullptr); + auto* set_read_func = + Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_FnOrThrow(&Ort::GetApi()); ASSERT_ORTSTATUS_OK(set_read_func(session_options, read_func, state)); } // Invokes the experimental EPContext write setter on the public C API. void SetEpContextDataWriteFunc(Ort::ModelCompilationOptions& compile_options, OrtWriteNamedBufferFunc write_func, void* state) { - auto set_write_func = - Ort::Experimental::Get_OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28_Fn( + auto* set_write_func = + Ort::Experimental::Get_OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28_FnOrThrow( &Ort::GetApi()); - ASSERT_NE(set_write_func, nullptr); ASSERT_ORTSTATUS_OK(set_write_func(compile_options, write_func, state)); } @@ -587,7 +585,8 @@ TEST(OrtEpLibrary, PluginEp_GenEpContextModel_EmbedModeDoesNotUseCallbacks) { EpContextDataCallbackState compile_read_callback_state; { Ort::SessionOptions session_options; - SetEpContextDataReadFunc(session_options, LoadEpContextDataCallback, &compile_read_callback_state); + ASSERT_NO_FATAL_FAILURE( + SetEpContextDataReadFunc(session_options, LoadEpContextDataCallback, &compile_read_callback_state)); std::unordered_map ep_options; session_options.AppendExecutionProvider_V2(*ort_env, {plugin_ep_device}, ep_options); @@ -597,7 +596,8 @@ TEST(OrtEpLibrary, PluginEp_GenEpContextModel_EmbedModeDoesNotUseCallbacks) { compile_options.SetInputModelPath(input_model_file); compile_options.SetOutputModelPath(output_model_file); compile_options.SetEpContextEmbedMode(true); - SetEpContextDataWriteFunc(compile_options, StoreEpContextDataCallback, &write_callback_state); + ASSERT_NO_FATAL_FAILURE( + SetEpContextDataWriteFunc(compile_options, StoreEpContextDataCallback, &write_callback_state)); ASSERT_CXX_ORTSTATUS_OK(Ort::CompileModel(*ort_env, compile_options)); } @@ -626,7 +626,8 @@ TEST(OrtEpLibrary, PluginEp_GenEpContextModel_EmbedModeDoesNotUseCallbacks) { EpContextDataCallbackState load_read_callback_state; { Ort::SessionOptions session_options; - SetEpContextDataReadFunc(session_options, LoadEpContextDataCallback, &load_read_callback_state); + ASSERT_NO_FATAL_FAILURE( + SetEpContextDataReadFunc(session_options, LoadEpContextDataCallback, &load_read_callback_state)); std::unordered_map ep_options; session_options.AppendExecutionProvider_V2(*ort_env, {plugin_ep_device}, ep_options); @@ -732,7 +733,7 @@ TEST(OrtEpLibrary, PluginEp_GenEpContextModel_ExternalDataUsesWriteCallback) { compile_options.SetInputModelPath(input_model_file); compile_options.SetOutputModelPath(output_model_file); compile_options.SetEpContextEmbedMode(false); - SetEpContextDataWriteFunc(compile_options, StoreEpContextDataCallback, &callback_state); + ASSERT_NO_FATAL_FAILURE(SetEpContextDataWriteFunc(compile_options, StoreEpContextDataCallback, &callback_state)); ASSERT_CXX_ORTSTATUS_OK(Ort::CompileModel(*ort_env, compile_options)); ASSERT_TRUE(std::filesystem::exists(output_model_file)); @@ -762,7 +763,8 @@ TEST(OrtEpLibrary, PluginEp_LoadEpContextModel_ExternalDataUsesReadCallback) { compile_options.SetInputModelPath(input_model_file); compile_options.SetOutputModelPath(compiled_model_file); compile_options.SetEpContextEmbedMode(false); - SetEpContextDataWriteFunc(compile_options, StoreEpContextDataCallback, &write_callback_state); + ASSERT_NO_FATAL_FAILURE( + SetEpContextDataWriteFunc(compile_options, StoreEpContextDataCallback, &write_callback_state)); ASSERT_CXX_ORTSTATUS_OK(Ort::CompileModel(*ort_env, compile_options)); ASSERT_TRUE(std::filesystem::exists(compiled_model_file)); @@ -773,7 +775,7 @@ TEST(OrtEpLibrary, PluginEp_LoadEpContextModel_ExternalDataUsesReadCallback) { read_callback_state.payload = write_callback_state.payload; { Ort::SessionOptions session_options; - SetEpContextDataReadFunc(session_options, LoadEpContextDataCallback, &read_callback_state); + ASSERT_NO_FATAL_FAILURE(SetEpContextDataReadFunc(session_options, LoadEpContextDataCallback, &read_callback_state)); std::unordered_map ep_options; session_options.AppendExecutionProvider_V2(*ort_env, {plugin_ep_device}, ep_options); diff --git a/onnxruntime/test/framework/ep_plugin_provider_test.cc b/onnxruntime/test/framework/ep_plugin_provider_test.cc index 240e619529004..1af90adcae26e 100644 --- a/onnxruntime/test/framework/ep_plugin_provider_test.cc +++ b/onnxruntime/test/framework/ep_plugin_provider_test.cc @@ -5,7 +5,6 @@ #include #include -#include #include #include #include @@ -62,45 +61,6 @@ static void CheckFileIsEmpty(const PathString& filename) { EXPECT_TRUE(content.empty()); } -// Asserts that `status_ptr` is a failure status with the expected error code and message substring. -// Takes ownership of `status_ptr` (wraps it in an Ort::Status that releases it on scope exit), so callers should pass -// the OrtStatus* returned directly by the API under test without releasing it themselves. -static void ExpectFailureOrtStatus(OrtStatus* status_ptr, OrtErrorCode expected_code, const char* expected_message) { - Ort::Status status{status_ptr}; - ASSERT_NE(status_ptr, nullptr) << "Expected a failure status, but the API returned nullptr (OK)."; - ASSERT_FALSE(status.IsOK()); - EXPECT_EQ(status.GetErrorCode(), expected_code); - EXPECT_THAT(status.GetErrorMessage(), ::testing::HasSubstr(expected_message)); -} - -struct EpContextReadCallbackState { - bool called = false; - std::string file_name; - std::vector payload; -}; - -static OrtStatus* ORT_API_CALL EpContextReadCallback(void* state, const char* file_name, OrtAllocator* allocator, - void** buffer, size_t* data_size) { - auto* read_state = static_cast(state); - read_state->called = true; - read_state->file_name = file_name; - - *buffer = nullptr; - *data_size = read_state->payload.size(); - - if (read_state->payload.empty()) { - return nullptr; - } - - OrtStatus* status = Ort::GetApi().AllocatorAlloc(allocator, read_state->payload.size(), buffer); - if (status != nullptr) { - return status; - } - - std::memcpy(*buffer, read_state->payload.data(), read_state->payload.size()); - return nullptr; -} - struct EpContextWriteCallbackState { bool called = false; std::string file_name; @@ -1783,215 +1743,23 @@ TEST(PluginExecutionProviderTest, GetGraphCaptureNodeAssignmentPolicy) { } } -TEST(PluginExecutionProviderTest, EpContextDataReadFuncIsReturnedByEpApi) { - const auto& ort_api = Ort::GetApi(); - Ort::SessionOptions session_options; - - auto set_read_func = Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_Fn(&ort_api); - ASSERT_NE(set_read_func, nullptr); - - EpContextReadCallbackState callback_state{ - false, - {}, - {'e', 'p', 'c', 't', 'x'}, - }; - ASSERT_ORTSTATUS_OK(set_read_func(session_options, EpContextReadCallback, &callback_state)); - - // The Ort::Experimental::EpContextConfig wrapper extracts the config from the session options and exposes the - // callbacks via GetReadFunc(), hiding the experimental Get_*_Fn lookups and the manual release. - Ort::Experimental::EpContextConfig ep_context_config{session_options}; - OrtReadNamedBufferFunc read_func = nullptr; - void* callback_state_out = nullptr; - ep_context_config.GetReadFunc(read_func, callback_state_out); - ASSERT_EQ(read_func, EpContextReadCallback); - ASSERT_EQ(callback_state_out, &callback_state); - - Ort::AllocatorWithDefaultOptions allocator; - void* buffer = nullptr; - size_t buffer_size = 0; - ASSERT_ORTSTATUS_OK(read_func(callback_state_out, "context.bin", allocator, &buffer, &buffer_size)); - auto release_buffer = gsl::finally([&]() { - if (buffer != nullptr) { - allocator.Free(buffer); - } - }); - - ASSERT_TRUE(callback_state.called); - EXPECT_EQ(callback_state.file_name, "context.bin"); - ASSERT_EQ(buffer_size, callback_state.payload.size()); - EXPECT_TRUE(std::equal(callback_state.payload.begin(), callback_state.payload.end(), - static_cast(buffer))); -} - -TEST(PluginExecutionProviderTest, EpContextDataApiRejectsInvalidArguments) { - const auto& ort_api = Ort::GetApi(); - - auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&ort_api); - auto release_config_func = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); - auto get_read_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&ort_api); - auto get_write_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); - auto set_read_func = Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_Fn(&ort_api); - ASSERT_NE(get_config, nullptr); - ASSERT_NE(release_config_func, nullptr); - ASSERT_NE(get_read_func, nullptr); - ASSERT_NE(get_write_func, nullptr); - ASSERT_NE(set_read_func, nullptr); - - Ort::SessionOptions session_options; - OrtEpContextConfig* ep_context_config = nullptr; - ExpectFailureOrtStatus(get_config(nullptr, &ep_context_config), ORT_INVALID_ARGUMENT, "OrtSessionOptions is NULL"); - ExpectFailureOrtStatus(get_config(session_options, nullptr), ORT_INVALID_ARGUMENT, - "Output OrtEpContextConfig is NULL"); - - ExpectFailureOrtStatus(set_read_func(nullptr, EpContextReadCallback, nullptr), - ORT_INVALID_ARGUMENT, "'options' parameter must not be NULL"); - // A null read_func is allowed: it clears any previously set callback (covered by - // EpContextDataReadFuncCanBeCleared), so it is not rejected here. - - ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); - auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); - - OrtReadNamedBufferFunc read_func = nullptr; - OrtWriteNamedBufferFunc write_func = nullptr; - void* state = nullptr; - ExpectFailureOrtStatus(get_read_func(nullptr, &read_func, &state), - ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); - ExpectFailureOrtStatus(get_read_func(ep_context_config, nullptr, &state), - ORT_INVALID_ARGUMENT, "Output read_func is NULL"); - ExpectFailureOrtStatus(get_read_func(ep_context_config, &read_func, nullptr), - ORT_INVALID_ARGUMENT, "Output state is NULL"); - ExpectFailureOrtStatus(get_write_func(nullptr, &write_func, &state), - ORT_INVALID_ARGUMENT, "OrtEpContextConfig is NULL"); - ExpectFailureOrtStatus(get_write_func(ep_context_config, nullptr, &state), - ORT_INVALID_ARGUMENT, "Output write_func is NULL"); - ExpectFailureOrtStatus(get_write_func(ep_context_config, &write_func, nullptr), - ORT_INVALID_ARGUMENT, "Output state is NULL"); - -#if !defined(ORT_MINIMAL_BUILD) - Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "EpContextDataApiRejectsInvalidArguments"}; - Ort::ModelCompilationOptions compilation_options{env, session_options}; - auto set_write_func = - Ort::Experimental::Get_OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); - ASSERT_NE(set_write_func, nullptr); - ExpectFailureOrtStatus(set_write_func(nullptr, EpContextWriteCallback, nullptr), - ORT_INVALID_ARGUMENT, "OrtModelCompilationOptions is NULL"); - ExpectFailureOrtStatus(set_write_func(compilation_options, nullptr, nullptr), - ORT_INVALID_ARGUMENT, "OrtWriteNamedBufferFunc function is NULL"); -#endif // !defined(ORT_MINIMAL_BUILD) -} - -TEST(PluginExecutionProviderTest, EpContextDataAccessorsReturnNullWhenCallbacksUnset) { - const auto& ort_api = Ort::GetApi(); - Ort::SessionOptions session_options; - - auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&ort_api); - auto release_config_func = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); - auto get_read_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&ort_api); - auto get_write_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); - ASSERT_NE(get_config, nullptr); - ASSERT_NE(release_config_func, nullptr); - ASSERT_NE(get_read_func, nullptr); - ASSERT_NE(get_write_func, nullptr); - - OrtEpContextConfig* ep_context_config = nullptr; - ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); - auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); - - OrtReadNamedBufferFunc read_func = EpContextReadCallback; - OrtWriteNamedBufferFunc write_func = EpContextWriteCallback; - void* state = reinterpret_cast(0x1); - - ASSERT_ORTSTATUS_OK(get_read_func(ep_context_config, &read_func, &state)); - EXPECT_EQ(read_func, nullptr); - EXPECT_EQ(state, nullptr); - - ASSERT_ORTSTATUS_OK(get_write_func(ep_context_config, &write_func, &state)); - EXPECT_EQ(write_func, nullptr); - EXPECT_EQ(state, nullptr); -} - -TEST(PluginExecutionProviderTest, EpContextConfigReturnsConfiguredCallbacks) { - const auto& ort_api = Ort::GetApi(); - Ort::SessionOptions session_options; - - auto set_read_func = Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_Fn(&ort_api); - auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&ort_api); - auto release_config_func = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); - auto get_read_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&ort_api); - auto get_write_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); - ASSERT_NE(set_read_func, nullptr); - ASSERT_NE(get_config, nullptr); - ASSERT_NE(release_config_func, nullptr); - ASSERT_NE(get_read_func, nullptr); - ASSERT_NE(get_write_func, nullptr); - - EpContextReadCallbackState callback_state{}; - ASSERT_ORTSTATUS_OK(set_read_func(session_options, EpContextReadCallback, &callback_state)); - - OrtEpContextConfig* ep_context_config = nullptr; - ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); - auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); - - OrtReadNamedBufferFunc read_func = nullptr; - void* read_state = nullptr; - ASSERT_ORTSTATUS_OK(get_read_func(ep_context_config, &read_func, &read_state)); - EXPECT_EQ(read_func, EpContextReadCallback); - EXPECT_EQ(read_state, &callback_state); - - OrtWriteNamedBufferFunc write_func = nullptr; - void* write_state = nullptr; - ASSERT_ORTSTATUS_OK(get_write_func(ep_context_config, &write_func, &write_state)); - EXPECT_EQ(write_func, nullptr); - EXPECT_EQ(write_state, nullptr); -} - -TEST(PluginExecutionProviderTest, EpContextDataReadFuncCanBeCleared) { - const auto& ort_api = Ort::GetApi(); - Ort::SessionOptions session_options; - - auto set_read_func = Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_Fn(&ort_api); - auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&ort_api); - auto release_config_func = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); - auto get_read_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&ort_api); - ASSERT_NE(set_read_func, nullptr); - ASSERT_NE(get_config, nullptr); - ASSERT_NE(release_config_func, nullptr); - ASSERT_NE(get_read_func, nullptr); - - EpContextReadCallbackState callback_state{}; - ASSERT_ORTSTATUS_OK(set_read_func(session_options, EpContextReadCallback, &callback_state)); - - // Passing a null read_func clears the callback. The previously set state must also be cleared so a stale state - // pointer is never paired with a missing callback. - ASSERT_ORTSTATUS_OK(set_read_func(session_options, nullptr, &callback_state)); - - OrtEpContextConfig* ep_context_config = nullptr; - ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); - auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); - - OrtReadNamedBufferFunc read_func = EpContextReadCallback; - void* read_state = reinterpret_cast(0x1); - ASSERT_ORTSTATUS_OK(get_read_func(ep_context_config, &read_func, &read_state)); - EXPECT_EQ(read_func, nullptr); - EXPECT_EQ(read_state, nullptr); -} - #if !defined(ORT_MINIMAL_BUILD) +// These framework tests intentionally inspect the internal session options held by ModelCompilationOptions. The pure +// public EPContext data API tests live in test/shared_lib/test_ep_context_data_api.cc. TEST(PluginExecutionProviderTest, EpContextDataWriteFuncIsReturnedByEpApi) { const auto& ort_api = Ort::GetApi(); Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "EpContextDataWriteFuncIsReturnedByEpApi"}; Ort::SessionOptions session_options; Ort::ModelCompilationOptions compilation_options{env, session_options}; - auto set_write_func = - Ort::Experimental::Get_OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); - auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&ort_api); - auto release_config_func = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); - auto get_write_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); - ASSERT_NE(set_write_func, nullptr); - ASSERT_NE(get_config, nullptr); - ASSERT_NE(release_config_func, nullptr); - ASSERT_NE(get_write_func, nullptr); + auto* set_write_func = + Ort::Experimental::Get_OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28_FnOrThrow( + &ort_api); + auto* get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_FnOrThrow(&ort_api); + auto* release_config_func = + Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_FnOrThrow(&ort_api); + auto* get_write_func = + Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_FnOrThrow(&ort_api); EpContextWriteCallbackState callback_state{}; ASSERT_ORTSTATUS_OK(set_write_func(compilation_options, EpContextWriteCallback, &callback_state)); @@ -2023,15 +1791,14 @@ TEST(PluginExecutionProviderTest, EpContextDataWriteFuncCanBeUsedWithEpContextBi Ort::SessionOptions session_options; Ort::ModelCompilationOptions compilation_options{env, session_options}; - auto set_write_func = - Ort::Experimental::Get_OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); - auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&ort_api); - auto release_config_func = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); - auto get_write_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn(&ort_api); - ASSERT_NE(set_write_func, nullptr); - ASSERT_NE(get_config, nullptr); - ASSERT_NE(release_config_func, nullptr); - ASSERT_NE(get_write_func, nullptr); + auto* set_write_func = + Ort::Experimental::Get_OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28_FnOrThrow( + &ort_api); + auto* get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_FnOrThrow(&ort_api); + auto* release_config_func = + Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_FnOrThrow(&ort_api); + auto* get_write_func = + Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_FnOrThrow(&ort_api); ASSERT_NO_THROW(compilation_options.SetEpContextBinaryInformation(ORT_TSTR("ep_context_dir/"), ORT_TSTR("compiled_model.onnx"))); @@ -2066,43 +1833,6 @@ TEST(PluginExecutionProviderTest, EpContextDataWriteFuncCanBeUsedWithEpContextBi } #endif // !defined(ORT_MINIMAL_BUILD) -TEST(PluginExecutionProviderTest, EpContextDataReturnedReadFuncAllowsEmptyPayloads) { - const auto& ort_api = Ort::GetApi(); - Ort::SessionOptions session_options; - - auto set_read_func = Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_Fn(&ort_api); - auto get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_Fn(&ort_api); - auto release_config_func = Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_Fn(&ort_api); - auto get_read_func = Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&ort_api); - ASSERT_NE(set_read_func, nullptr); - ASSERT_NE(get_config, nullptr); - ASSERT_NE(release_config_func, nullptr); - ASSERT_NE(get_read_func, nullptr); - - EpContextReadCallbackState callback_state{}; - ASSERT_ORTSTATUS_OK(set_read_func(session_options, EpContextReadCallback, &callback_state)); - - OrtEpContextConfig* ep_context_config = nullptr; - ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); - auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); - - OrtReadNamedBufferFunc read_func = nullptr; - void* read_state = nullptr; - ASSERT_ORTSTATUS_OK(get_read_func(ep_context_config, &read_func, &read_state)); - ASSERT_EQ(read_func, EpContextReadCallback); - ASSERT_EQ(read_state, &callback_state); - - Ort::AllocatorWithDefaultOptions allocator; - void* buffer = reinterpret_cast(0x1); - size_t buffer_size = 1; - ASSERT_ORTSTATUS_OK(read_func(read_state, "empty.bin", allocator, &buffer, &buffer_size)); - - EXPECT_TRUE(callback_state.called); - EXPECT_EQ(callback_state.file_name, "empty.bin"); - EXPECT_EQ(buffer, nullptr); - EXPECT_EQ(buffer_size, 0U); -} - // Helper: create a no-threshold resource accountant via the real factory (config ","). static IResourceAccountant* CreateNoThresholdAccountant(std::optional& acc_map) { ConfigOptions config; diff --git a/onnxruntime/test/shared_lib/test_ep_context_data_api.cc b/onnxruntime/test/shared_lib/test_ep_context_data_api.cc new file mode 100644 index 0000000000000..3656b955405cb --- /dev/null +++ b/onnxruntime/test/shared_lib/test_ep_context_data_api.cc @@ -0,0 +1,323 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include +#include +#include +#include + +#include "core/session/onnxruntime_c_api.h" +#include "core/session/onnxruntime_cxx_api.h" +#include "core/session/onnxruntime_experimental_cxx_api.h" + +#include "gmock/gmock.h" +#include "gsl/gsl" +#include "gtest/gtest.h" +#include "test/util/include/api_asserts.h" + +namespace { + +void ExpectFailureOrtStatus(OrtStatus* status_ptr, OrtErrorCode expected_code, const char* expected_message) { + Ort::Status status{status_ptr}; + ASSERT_NE(status_ptr, nullptr) << "Expected a failure status, but the API returned nullptr (OK)."; + ASSERT_FALSE(status.IsOK()); + EXPECT_EQ(status.GetErrorCode(), expected_code); + EXPECT_THAT(status.GetErrorMessage(), ::testing::HasSubstr(expected_message)); +} + +struct EpContextReadCallbackState { + bool called = false; + std::string file_name; + std::vector payload; +}; + +OrtStatus* ORT_API_CALL EpContextReadCallback(void* state, const char* file_name, OrtAllocator* allocator, + void** buffer, size_t* data_size) { + auto* read_state = static_cast(state); + read_state->called = true; + read_state->file_name = file_name; + + *buffer = nullptr; + *data_size = read_state->payload.size(); + + if (read_state->payload.empty()) { + return nullptr; + } + + OrtStatus* status = Ort::GetApi().AllocatorAlloc(allocator, read_state->payload.size(), buffer); + if (status != nullptr) { + return status; + } + + std::memcpy(*buffer, read_state->payload.data(), read_state->payload.size()); + return nullptr; +} + +struct EpContextWriteCallbackState { + bool called = false; + std::string file_name; + std::vector payload; +}; + +OrtStatus* ORT_API_CALL EpContextWriteCallback(void* state, const char* file_name, const void* buffer, + size_t buffer_size) { + auto* write_state = static_cast(state); + write_state->called = true; + write_state->file_name = file_name; + write_state->payload.clear(); + if (buffer_size != 0) { + if (buffer == nullptr) { + return Ort::GetApi().CreateStatus(ORT_INVALID_ARGUMENT, + "EpContextWriteCallback received a null buffer for non-empty data"); + } + + const char* buffer_bytes = static_cast(buffer); + write_state->payload.assign(buffer_bytes, buffer_bytes + buffer_size); + } + + return nullptr; +} + +} // namespace + +TEST(EpContextDataApiTest, ReadFuncIsReturnedByEpApi) { + const auto& ort_api = Ort::GetApi(); + Ort::SessionOptions session_options; + + auto* set_read_func = + Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_FnOrThrow(&ort_api); + + EpContextReadCallbackState callback_state{ + false, + {}, + {'e', 'p', 'c', 't', 'x'}, + }; + ASSERT_ORTSTATUS_OK(set_read_func(session_options, EpContextReadCallback, &callback_state)); + + Ort::Experimental::EpContextConfig ep_context_config{session_options}; + OrtReadNamedBufferFunc read_func = nullptr; + void* callback_state_out = nullptr; + ep_context_config.GetReadFunc(read_func, callback_state_out); + ASSERT_EQ(read_func, EpContextReadCallback); + ASSERT_EQ(callback_state_out, &callback_state); + + Ort::AllocatorWithDefaultOptions allocator; + void* buffer = nullptr; + size_t buffer_size = 0; + ASSERT_ORTSTATUS_OK(read_func(callback_state_out, "context.bin", allocator, &buffer, &buffer_size)); + auto release_buffer = gsl::finally([&]() { + if (buffer != nullptr) { + allocator.Free(buffer); + } + }); + + ASSERT_TRUE(callback_state.called); + EXPECT_EQ(callback_state.file_name, "context.bin"); + ASSERT_EQ(buffer_size, callback_state.payload.size()); + EXPECT_TRUE(std::equal(callback_state.payload.begin(), callback_state.payload.end(), + static_cast(buffer))); +} + +TEST(EpContextDataApiTest, ApiRejectsInvalidArguments) { + const auto& ort_api = Ort::GetApi(); + + auto* get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_FnOrThrow(&ort_api); + auto* release_config_func = + Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_FnOrThrow(&ort_api); + auto* get_read_func = + Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_FnOrThrow(&ort_api); + auto* get_write_func = + Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_FnOrThrow(&ort_api); + auto* set_read_func = + Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_FnOrThrow(&ort_api); + + Ort::SessionOptions session_options; + OrtEpContextConfig* ep_context_config = nullptr; + ExpectFailureOrtStatus(get_config(nullptr, &ep_context_config), ORT_INVALID_ARGUMENT, "OrtSessionOptions is NULL"); + ExpectFailureOrtStatus(get_config(session_options, nullptr), ORT_INVALID_ARGUMENT, + "Output OrtEpContextConfig is NULL"); + + ExpectFailureOrtStatus(set_read_func(nullptr, EpContextReadCallback, nullptr), ORT_INVALID_ARGUMENT, + "'options' parameter must not be NULL"); + + ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); + auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); + + OrtReadNamedBufferFunc read_func = nullptr; + OrtWriteNamedBufferFunc write_func = nullptr; + void* state = nullptr; + ExpectFailureOrtStatus(get_read_func(nullptr, &read_func, &state), ORT_INVALID_ARGUMENT, + "OrtEpContextConfig is NULL"); + ExpectFailureOrtStatus(get_read_func(ep_context_config, nullptr, &state), ORT_INVALID_ARGUMENT, + "Output read_func is NULL"); + ExpectFailureOrtStatus(get_read_func(ep_context_config, &read_func, nullptr), ORT_INVALID_ARGUMENT, + "Output state is NULL"); + ExpectFailureOrtStatus(get_write_func(nullptr, &write_func, &state), ORT_INVALID_ARGUMENT, + "OrtEpContextConfig is NULL"); + ExpectFailureOrtStatus(get_write_func(ep_context_config, nullptr, &state), ORT_INVALID_ARGUMENT, + "Output write_func is NULL"); + ExpectFailureOrtStatus(get_write_func(ep_context_config, &write_func, nullptr), ORT_INVALID_ARGUMENT, + "Output state is NULL"); + +#if !defined(ORT_MINIMAL_BUILD) + Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "EpContextDataApiRejectsInvalidArguments"}; + Ort::ModelCompilationOptions compilation_options{env, session_options}; + auto* set_write_func = + Ort::Experimental::Get_OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28_FnOrThrow( + &ort_api); + ExpectFailureOrtStatus(set_write_func(nullptr, EpContextWriteCallback, nullptr), ORT_INVALID_ARGUMENT, + "OrtModelCompilationOptions is NULL"); + ExpectFailureOrtStatus(set_write_func(compilation_options, nullptr, nullptr), ORT_INVALID_ARGUMENT, + "OrtWriteNamedBufferFunc function is NULL"); +#endif // !defined(ORT_MINIMAL_BUILD) +} + +TEST(EpContextDataApiTest, AccessorsReturnNullWhenCallbacksUnset) { + const auto& ort_api = Ort::GetApi(); + Ort::SessionOptions session_options; + + auto* get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_FnOrThrow(&ort_api); + auto* release_config_func = + Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_FnOrThrow(&ort_api); + auto* get_read_func = + Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_FnOrThrow(&ort_api); + auto* get_write_func = + Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_FnOrThrow(&ort_api); + + OrtEpContextConfig* ep_context_config = nullptr; + ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); + auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); + + OrtReadNamedBufferFunc read_func = EpContextReadCallback; + OrtWriteNamedBufferFunc write_func = EpContextWriteCallback; + void* state = reinterpret_cast(0x1); + + ASSERT_ORTSTATUS_OK(get_read_func(ep_context_config, &read_func, &state)); + EXPECT_EQ(read_func, nullptr); + EXPECT_EQ(state, nullptr); + + ASSERT_ORTSTATUS_OK(get_write_func(ep_context_config, &write_func, &state)); + EXPECT_EQ(write_func, nullptr); + EXPECT_EQ(state, nullptr); +} + +TEST(EpContextDataApiTest, ConfigReturnsConfiguredCallbacks) { + const auto& ort_api = Ort::GetApi(); + Ort::SessionOptions session_options; + + auto* set_read_func = + Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_FnOrThrow(&ort_api); + auto* get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_FnOrThrow(&ort_api); + auto* release_config_func = + Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_FnOrThrow(&ort_api); + auto* get_read_func = + Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_FnOrThrow(&ort_api); + auto* get_write_func = + Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_FnOrThrow(&ort_api); + + EpContextReadCallbackState callback_state{}; + ASSERT_ORTSTATUS_OK(set_read_func(session_options, EpContextReadCallback, &callback_state)); + + OrtEpContextConfig* ep_context_config = nullptr; + ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); + auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); + + OrtReadNamedBufferFunc read_func = nullptr; + void* read_state = nullptr; + ASSERT_ORTSTATUS_OK(get_read_func(ep_context_config, &read_func, &read_state)); + EXPECT_EQ(read_func, EpContextReadCallback); + EXPECT_EQ(read_state, &callback_state); + + OrtWriteNamedBufferFunc write_func = nullptr; + void* write_state = nullptr; + ASSERT_ORTSTATUS_OK(get_write_func(ep_context_config, &write_func, &write_state)); + EXPECT_EQ(write_func, nullptr); + EXPECT_EQ(write_state, nullptr); +} + +TEST(EpContextDataApiTest, ReadFuncCanBeCleared) { + const auto& ort_api = Ort::GetApi(); + Ort::SessionOptions session_options; + + auto* set_read_func = + Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_FnOrThrow(&ort_api); + auto* get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_FnOrThrow(&ort_api); + auto* release_config_func = + Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_FnOrThrow(&ort_api); + auto* get_read_func = + Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_FnOrThrow(&ort_api); + + EpContextReadCallbackState callback_state{}; + ASSERT_ORTSTATUS_OK(set_read_func(session_options, EpContextReadCallback, &callback_state)); + + ASSERT_ORTSTATUS_OK(set_read_func(session_options, nullptr, &callback_state)); + + OrtEpContextConfig* ep_context_config = nullptr; + ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); + auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); + + OrtReadNamedBufferFunc read_func = EpContextReadCallback; + void* read_state = reinterpret_cast(0x1); + ASSERT_ORTSTATUS_OK(get_read_func(ep_context_config, &read_func, &read_state)); + EXPECT_EQ(read_func, nullptr); + EXPECT_EQ(read_state, nullptr); +} + +#if !defined(ORT_MINIMAL_BUILD) +TEST(EpContextDataApiTest, WriteFuncCanBeSetOnModelCompilationOptions) { + const auto& ort_api = Ort::GetApi(); + Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "EpContextDataWriteFuncCanBeSetOnModelCompilationOptions"}; + Ort::SessionOptions session_options; + Ort::ModelCompilationOptions compilation_options{env, session_options}; + + auto* set_write_func = + Ort::Experimental::Get_OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28_FnOrThrow( + &ort_api); + + EpContextWriteCallbackState callback_state{}; + ASSERT_ORTSTATUS_OK(set_write_func(compilation_options, EpContextWriteCallback, &callback_state)); + + const std::vector payload{'b', 'i', 'n', 'a', 'r', 'y'}; + ASSERT_ORTSTATUS_OK(EpContextWriteCallback(&callback_state, "engine.bin", payload.data(), payload.size())); + + ASSERT_TRUE(callback_state.called); + EXPECT_EQ(callback_state.file_name, "engine.bin"); + EXPECT_EQ(callback_state.payload, payload); +} +#endif // !defined(ORT_MINIMAL_BUILD) + +TEST(EpContextDataApiTest, ReturnedReadFuncAllowsEmptyPayloads) { + const auto& ort_api = Ort::GetApi(); + Ort::SessionOptions session_options; + + auto* set_read_func = + Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_FnOrThrow(&ort_api); + auto* get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_FnOrThrow(&ort_api); + auto* release_config_func = + Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_FnOrThrow(&ort_api); + auto* get_read_func = + Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_FnOrThrow(&ort_api); + + EpContextReadCallbackState callback_state{}; + ASSERT_ORTSTATUS_OK(set_read_func(session_options, EpContextReadCallback, &callback_state)); + + OrtEpContextConfig* ep_context_config = nullptr; + ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); + auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); + + OrtReadNamedBufferFunc read_func = nullptr; + void* read_state = nullptr; + ASSERT_ORTSTATUS_OK(get_read_func(ep_context_config, &read_func, &read_state)); + ASSERT_EQ(read_func, EpContextReadCallback); + ASSERT_EQ(read_state, &callback_state); + + Ort::AllocatorWithDefaultOptions allocator; + void* buffer = reinterpret_cast(0x1); + size_t buffer_size = 1; + ASSERT_ORTSTATUS_OK(read_func(read_state, "empty.bin", allocator, &buffer, &buffer_size)); + + EXPECT_TRUE(callback_state.called); + EXPECT_EQ(callback_state.file_name, "empty.bin"); + EXPECT_EQ(buffer, nullptr); + EXPECT_EQ(buffer_size, 0U); +} From 7ab31a155d25c5ff5706eea1ab486a0223a9a1e1 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Thu, 25 Jun 2026 14:07:56 -0700 Subject: [PATCH 43/48] Address EPContext review comments (latest round) - Fix doc to reference OrtCompileApi (not OrtApi) for ModelCompilationOptions_SetEpContextBinaryInformation. - EpContextConfig: drop the default and raw OrtSessionOptions* constructors; keep only the SessionOptions/ConstSessionOptions overloads (wrap at the ep_factory call site). - example EP: comment that the load-side EPContext read during compile is intentionally exercised then discarded. - ep_context_data_utils: low-level *WithFileFallback overloads now take the callback + state directly; high-level overloads extract them from OrtEpContextConfig. - Allow a NULL EPContext write callback to clear a previously set one (symmetric with the read setter); update doc/SAL and tests. - Move the public EPContext write-callback tests out of the framework provider test into test/shared_lib/test_ep_context_data_api.cc. --- .../onnxruntime_experimental_c_api.inc | 19 +-- .../onnxruntime_experimental_cxx_api.h | 7 +- .../core/session/experimental_c_api.cc | 5 +- .../core/session/model_compilation_options.cc | 4 +- .../test/autoep/ep_context_data_utils_test.cc | 54 +-------- .../autoep/library/ep_context_data_utils.h | 84 +++++-------- .../autoep/library/example_plugin_ep/ep.cc | 3 + .../library/example_plugin_ep/ep_factory.cc | 2 +- .../test/framework/ep_plugin_provider_test.cc | 114 ------------------ .../shared_lib/test_ep_context_data_api.cc | 48 +++++++- 10 files changed, 106 insertions(+), 234 deletions(-) diff --git a/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc b/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc index bcfc22becb152..b1140485f7ff1 100644 --- a/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc +++ b/include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc @@ -311,8 +311,8 @@ ORT_EXPERIMENTAL_API(28, OrtStatusPtr, OrtApi_SessionOptions_SetEpContextDataRea * When EPContext embed mode is disabled, execution providers can retrieve this callback from OrtEpContextConfig and * call it instead of writing EPContext binary data directly to disk. * - * This callback may be used together with OrtApi::ModelCompilationOptions_SetEpContextBinaryInformation. The binary - * information still describes the compiled model/output location that EPs may use to generate stable logical + * This callback may be used together with OrtCompileApi::ModelCompilationOptions_SetEpContextBinaryInformation. The + * binary information still describes the compiled model/output location that EPs may use to generate stable logical * EPContext data names or as a file-fallback location. If this callback is configured, EPs should call it for * EPContext binary data instead of writing that data to the fallback file path. * @@ -324,19 +324,20 @@ ORT_EXPERIMENTAL_API(28, OrtStatusPtr, OrtApi_SessionOptions_SetEpContextDataRea * operation that may call the callback. If the same state may be used by multiple EPs or threads, the application is * responsible for synchronization. * - * Unlike OrtApi_SessionOptions_SetEpContextDataReadFunc, which accepts a NULL callback to clear a previously set one, - * write_func must not be NULL here: compilation options are configured once per compile, so there is no prior - * callback to clear and a NULL write_func is rejected with ORT_INVALID_ARGUMENT. + * Like OrtApi_SessionOptions_SetEpContextDataReadFunc, passing a NULL write_func clears any previously set callback + * (any previously set state is cleared as well). Calling this multiple times overwrites the previously configured + * callback. * * \param[in] model_compile_options The OrtModelCompilationOptions instance. - * \param[in] write_func The OrtWriteNamedBufferFunc callback used to write EPContext bytes. Must not be NULL. - * \param[in] state Opaque state passed to write_func. Can be NULL. + * \param[in] write_func The OrtWriteNamedBufferFunc callback used to write EPContext bytes. Pass NULL to clear a + * previously set callback (any previously set state is cleared as well). + * \param[in] state Opaque state passed to write_func. Can be NULL. Ignored when write_func is NULL. * * \snippet{doc} snippets.dox OrtStatus Return Value */ ORT_EXPERIMENTAL_API(28, OrtStatusPtr, OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc, - _In_ OrtModelCompilationOptions* model_compile_options, _In_ OrtWriteNamedBufferFunc write_func, - _In_opt_ void* state) + _In_ OrtModelCompilationOptions* model_compile_options, + _In_opt_ OrtWriteNamedBufferFunc write_func, _In_opt_ void* state) /** \brief Extracts the EPContext configuration (callbacks and state) from an OrtSessionOptions instance. * diff --git a/include/onnxruntime/core/session/onnxruntime_experimental_cxx_api.h b/include/onnxruntime/core/session/onnxruntime_experimental_cxx_api.h index bd6f86a1a4606..d3f3a37eca33d 100644 --- a/include/onnxruntime/core/session/onnxruntime_experimental_cxx_api.h +++ b/include/onnxruntime/core/session/onnxruntime_experimental_cxx_api.h @@ -116,21 +116,18 @@ namespace Experimental { // query the callbacks via GetReadFunc() / GetWriteFunc(). class EpContextConfig { public: - EpContextConfig() noexcept = default; explicit EpContextConfig(std::nullptr_t) noexcept {} explicit EpContextConfig(const SessionOptions& session_options) : EpContextConfig{session_options.GetConst()} {} - explicit EpContextConfig(ConstSessionOptions session_options) - : EpContextConfig{static_cast(session_options)} {} // Extracts the EPContext config from `session_options`. Throws Ort::Exception (ORT_NOT_IMPLEMENTED) if the // experimental functions are not available in this build, or propagates any error from the extraction. - explicit EpContextConfig(const OrtSessionOptions* session_options) { + explicit EpContextConfig(ConstSessionOptions session_options) { const OrtApi* api = &GetApi(); // Ensure the release function is available before creating a handle, so the handle can always be freed. Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_FnOrThrow(api); auto* get_config = Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_FnOrThrow(api); - ThrowOnError(get_config(session_options, &config_)); + ThrowOnError(get_config(static_cast(session_options), &config_)); } EpContextConfig(EpContextConfig&& other) noexcept : config_{other.config_} { other.config_ = nullptr; } diff --git a/onnxruntime/core/session/experimental_c_api.cc b/onnxruntime/core/session/experimental_c_api.cc index 950bb3a022a6b..a17ca4414ff4e 100644 --- a/onnxruntime/core/session/experimental_c_api.cc +++ b/onnxruntime/core/session/experimental_c_api.cc @@ -72,12 +72,13 @@ ORT_API_STATUS_IMPL(OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28, _In ORT_API_STATUS_IMPL(OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28, _In_ OrtModelCompilationOptions* ort_model_compile_options, - _In_ OrtWriteNamedBufferFunc write_func, _In_opt_ void* state) { + _In_opt_ OrtWriteNamedBufferFunc write_func, _In_opt_ void* state) { API_IMPL_BEGIN #if !defined(ORT_MINIMAL_BUILD) ORT_API_RETURN_IF(ort_model_compile_options == nullptr, ORT_INVALID_ARGUMENT, "OrtModelCompilationOptions is NULL"); - ORT_API_RETURN_IF(write_func == nullptr, ORT_INVALID_ARGUMENT, "OrtWriteNamedBufferFunc function is NULL"); + // A null write_func clears any previously set callback (symmetric with OrtApi_SessionOptions_SetEpContextDataReadFunc + // and consistent with calling this multiple times to overwrite the callback). auto model_compile_options = reinterpret_cast(ort_model_compile_options); model_compile_options->SetEpContextDataWriteFunc(write_func, state); return nullptr; diff --git a/onnxruntime/core/session/model_compilation_options.cc b/onnxruntime/core/session/model_compilation_options.cc index 32dd4a4a77e9c..83cd57b9490b5 100644 --- a/onnxruntime/core/session/model_compilation_options.cc +++ b/onnxruntime/core/session/model_compilation_options.cc @@ -134,9 +134,11 @@ void ModelCompilationOptions::SetOutputModelGetInitializerLocationFunc( } void ModelCompilationOptions::SetEpContextDataWriteFunc(OrtWriteNamedBufferFunc write_func, void* state) { + // A null write_func clears any previously set callback. Clear the state too so a stale state pointer is never + // paired with a missing callback. session_options_.value.ep_context_gen_options.ep_context_data_write_func = epctx::EpContextDataWriteFuncHolder{ write_func, - state, + write_func != nullptr ? state : nullptr, }; } diff --git a/onnxruntime/test/autoep/ep_context_data_utils_test.cc b/onnxruntime/test/autoep/ep_context_data_utils_test.cc index 1bd17d6801835..6c9613a8f0b5d 100644 --- a/onnxruntime/test/autoep/ep_context_data_utils_test.cc +++ b/onnxruntime/test/autoep/ep_context_data_utils_test.cc @@ -53,40 +53,6 @@ std::filesystem::path PrepareTempTestDir(std::string_view name) { return test_dir; } -struct FakeEpContextConfigCallbacks { - OrtReadNamedBufferFunc read_func = nullptr; - void* read_state = nullptr; - OrtWriteNamedBufferFunc write_func = nullptr; - void* write_state = nullptr; -}; - -// The low-level *WithFileFallback overloads treat the OrtEpContextConfig as an opaque token that is only forwarded -// to the injected getter; they never dereference it. These tests therefore pair a real (empty) OrtEpContextConfig -// with getters that return callbacks from this holder, instead of reinterpret_casting a foreign struct to -// OrtEpContextConfig* (which is undefined behavior). gtest runs these serially, so a single shared pointer is safe. -const FakeEpContextConfigCallbacks* g_fake_ep_context_callbacks = nullptr; - -OrtStatus* ORT_API_CALL FakeEpContextConfigGetReadFunc(const OrtEpContextConfig* /*config*/, - OrtReadNamedBufferFunc* read_func, void** state) noexcept { - *read_func = g_fake_ep_context_callbacks->read_func; - *state = g_fake_ep_context_callbacks->read_state; - return nullptr; -} - -OrtStatus* ORT_API_CALL FakeEpContextConfigGetWriteFunc(const OrtEpContextConfig* /*config*/, - OrtWriteNamedBufferFunc* write_func, void** state) noexcept { - *write_func = g_fake_ep_context_callbacks->write_func; - *state = g_fake_ep_context_callbacks->write_state; - return nullptr; -} - -// Returns a real, empty OrtEpContextConfig (owned by the returned wrapper) to use as the opaque token passed to the -// fake getters above. -Ort::Experimental::EpContextConfig MakeEmptyEpContextConfig() { - Ort::SessionOptions session_options; - return Ort::Experimental::EpContextConfig{session_options}; -} - } // namespace TEST(OrtEpLibrary, EpContextDataUtils_PathHelpersRoundTrip) { @@ -302,31 +268,27 @@ TEST(OrtEpLibrary, EpContextDataUtils_CallbackFallbackUsesCallbacks) { EpContextDataCallbackState read_callback_state; read_callback_state.payload = {'c', 'a', 'l', 'l', 'b', 'a', 'c', 'k'}; EpContextDataCallbackState write_callback_state; - FakeEpContextConfigCallbacks callbacks{LoadEpContextDataCallback, &read_callback_state, - StoreEpContextDataCallback, &write_callback_state}; - g_fake_ep_context_callbacks = &callbacks; - auto reset_fake_callbacks = gsl::finally([]() { g_fake_ep_context_callbacks = nullptr; }); - const Ort::Experimental::EpContextConfig fake_config = MakeEmptyEpContextConfig(); std::vector data; ASSERT_ORTSTATUS_OK(ep_context_data_utils::ReadEpContextDataWithFileFallback( - api, FakeEpContextConfigGetReadFunc, fake_config.get(), "callback_context.bin", nullptr, data)); + api, LoadEpContextDataCallback, &read_callback_state, "callback_context.bin", nullptr, data)); ASSERT_TRUE(read_callback_state.read_called); EXPECT_EQ(read_callback_state.read_file_name, "callback_context.bin"); EXPECT_EQ(data, read_callback_state.payload); const std::string payload = "callback write payload"; ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( - api, FakeEpContextConfigGetWriteFunc, fake_config.get(), "callback_write_context.bin", nullptr, - payload.data(), payload.size())); + api, StoreEpContextDataCallback, &write_callback_state, "callback_write_context.bin", + "callback_write_context.bin", nullptr, payload.data(), payload.size())); ASSERT_TRUE(write_callback_state.write_called); EXPECT_EQ(write_callback_state.write_file_name, "callback_write_context.bin"); EXPECT_EQ(std::string(write_callback_state.payload.begin(), write_callback_state.payload.end()), payload); write_callback_state = {}; const std::string payload_with_unused_fallback = "callback write payload with unused fallback"; + // With a callback present the file fallback is never used, so the empty fallback name is accepted (not validated). ASSERT_ORTSTATUS_OK(ep_context_data_utils::WriteEpContextDataWithFileFallback( - api, FakeEpContextConfigGetWriteFunc, fake_config.get(), + api, StoreEpContextDataCallback, &write_callback_state, "callback_write_context_unused_fallback.bin", "", nullptr, payload_with_unused_fallback.data(), payload_with_unused_fallback.size())); ASSERT_TRUE(write_callback_state.write_called); @@ -339,14 +301,10 @@ TEST(OrtEpLibrary, EpContextDataUtils_ReadCallbackRejectsNullBufferForNonEmptyPa const auto& api = Ort::GetApi(); EpContextDataCallbackState read_callback_state; - FakeEpContextConfigCallbacks callbacks{LoadInvalidEpContextDataCallback, &read_callback_state, nullptr, nullptr}; - g_fake_ep_context_callbacks = &callbacks; - auto reset_fake_callbacks = gsl::finally([]() { g_fake_ep_context_callbacks = nullptr; }); - const Ort::Experimental::EpContextConfig fake_config = MakeEmptyEpContextConfig(); std::vector data; ExpectOrtStatusError(ep_context_data_utils::ReadEpContextDataWithFileFallback( - api, FakeEpContextConfigGetReadFunc, fake_config.get(), + api, LoadInvalidEpContextDataCallback, &read_callback_state, "invalid_callback_context.bin", nullptr, data), ORT_FAIL, "OrtReadNamedBufferFunc returned a null buffer for non-empty EPContext data"); ASSERT_TRUE(read_callback_state.read_called); diff --git a/onnxruntime/test/autoep/library/ep_context_data_utils.h b/onnxruntime/test/autoep/library/ep_context_data_utils.h index 33ce0ac0cf5da..646f74634a4ae 100644 --- a/onnxruntime/test/autoep/library/ep_context_data_utils.h +++ b/onnxruntime/test/autoep/library/ep_context_data_utils.h @@ -39,11 +39,6 @@ // size limits, and path policies; see the per-function notes on how untrusted, model-derived names are treated. namespace ep_context_data_utils { -using GetEpContextDataReadFunc = OrtStatus*(ORT_API_CALL*)(const OrtEpContextConfig* config, - OrtReadNamedBufferFunc* read_func, void** state); -using GetEpContextDataWriteFunc = OrtStatus*(ORT_API_CALL*)(const OrtEpContextConfig* config, - OrtWriteNamedBufferFunc* write_func, void** state); - #ifdef _WIN32 inline std::string WindowsLastErrorMessage(std::string_view message, DWORD error_code) { return std::string{message} + " GetLastError=" + std::to_string(error_code); @@ -336,29 +331,18 @@ inline OrtStatus* WriteEpContextDataToFile(const OrtApi& api, const char* file_n return WriteEpContextDataToResolvedFile(api, data_path, buffer, buffer_size); } -// Low-level overload that takes the GetEpContextDataReadFunc accessor explicitly. Production code should use the -// overload below that derives the accessor from `api`; this overload exists so unit tests can inject a fake accessor. -// `ep_context_config` is an opaque token only forwarded to `get_read_func` (never dereferenced here). +// Low-level overload that takes the read callback and its opaque state directly. Production EPs should use the +// overload below that takes an OrtEpContextConfig; this overload exists so unit tests can inject a callback without +// constructing an OrtEpContextConfig. When `read_func` is null the data is read from a file. inline OrtStatus* ReadEpContextDataWithFileFallback( const OrtApi& api, - GetEpContextDataReadFunc get_read_func, - const OrtEpContextConfig* ep_context_config, + OrtReadNamedBufferFunc read_func, void* read_state, const char* file_name, const OrtGraph* graph, std::vector& data) { if (file_name == nullptr || file_name[0] == '\0') { return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be empty"); } - OrtReadNamedBufferFunc read_func = nullptr; - void* read_state = nullptr; - if (ep_context_config != nullptr && get_read_func == nullptr) { - return api.CreateStatus(ORT_NOT_IMPLEMENTED, - "OrtEpApi_EpContextConfig_GetEpContextDataReadFunc is not available"); - } - if (ep_context_config != nullptr) { - RETURN_IF_ERROR(get_read_func(ep_context_config, &read_func, &read_state)); - } - if (read_func == nullptr) { return ReadEpContextDataFromFile(api, file_name, graph, data); } @@ -402,19 +386,26 @@ inline OrtStatus* ReadEpContextDataWithFileFallback( const OrtEpContextConfig* ep_context_config, const char* file_name, const OrtGraph* graph, std::vector& data) { - return ReadEpContextDataWithFileFallback( - api, - Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&api), - ep_context_config, file_name, graph, data); + OrtReadNamedBufferFunc read_func = nullptr; + void* read_state = nullptr; + if (ep_context_config != nullptr) { + auto get_read_func = + Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_Fn(&api); + if (get_read_func == nullptr) { + return api.CreateStatus(ORT_NOT_IMPLEMENTED, + "OrtEpApi_EpContextConfig_GetEpContextDataReadFunc is not available"); + } + RETURN_IF_ERROR(get_read_func(ep_context_config, &read_func, &read_state)); + } + return ReadEpContextDataWithFileFallback(api, read_func, read_state, file_name, graph, data); } -// Low-level overload that takes the GetEpContextDataWriteFunc accessor explicitly. Production code should use the -// overload below that derives the accessor from `api`; this overload exists so unit tests can inject a fake accessor. -// `ep_context_config` is an opaque token only forwarded to `get_write_func` (never dereferenced here). +// Low-level overload that takes the write callback and its opaque state directly. Production EPs should use the +// overloads below that take an OrtEpContextConfig; this overload exists so unit tests can inject a callback without +// constructing an OrtEpContextConfig. When `write_func` is null the data is written to the file fallback. inline OrtStatus* WriteEpContextDataWithFileFallback( const OrtApi& api, - GetEpContextDataWriteFunc get_write_func, - const OrtEpContextConfig* ep_context_config, + OrtWriteNamedBufferFunc write_func, void* write_state, const char* file_name, const char* fallback_file_name, const OrtGraph* graph, const void* buffer, size_t buffer_size) { @@ -426,16 +417,6 @@ inline OrtStatus* WriteEpContextDataWithFileFallback( return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data buffer must not be null for non-empty data"); } - OrtWriteNamedBufferFunc write_func = nullptr; - void* write_state = nullptr; - if (ep_context_config != nullptr && get_write_func == nullptr) { - return api.CreateStatus(ORT_NOT_IMPLEMENTED, - "OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc is not available"); - } - if (ep_context_config != nullptr) { - RETURN_IF_ERROR(get_write_func(ep_context_config, &write_func, &write_state)); - } - // The app-supplied write callback owns its own logical namespace, so file_name is passed through unmodified. // Only the file-fallback path below maps a name onto the filesystem, so it validates the logical name there. if (write_func != nullptr) { @@ -468,19 +449,18 @@ inline OrtStatus* WriteEpContextDataWithFileFallback( const char* file_name, const char* fallback_file_name, const OrtGraph* graph, const void* buffer, size_t buffer_size) { - return WriteEpContextDataWithFileFallback( - api, - Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn(&api), - ep_context_config, file_name, fallback_file_name, graph, buffer, buffer_size); -} - -inline OrtStatus* WriteEpContextDataWithFileFallback( - const OrtApi& api, - GetEpContextDataWriteFunc get_write_func, - const OrtEpContextConfig* ep_context_config, - const char* file_name, const OrtGraph* graph, - const void* buffer, size_t buffer_size) { - return WriteEpContextDataWithFileFallback(api, get_write_func, ep_context_config, file_name, file_name, graph, buffer, + OrtWriteNamedBufferFunc write_func = nullptr; + void* write_state = nullptr; + if (ep_context_config != nullptr) { + auto get_write_func = + Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_Fn(&api); + if (get_write_func == nullptr) { + return api.CreateStatus(ORT_NOT_IMPLEMENTED, + "OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc is not available"); + } + RETURN_IF_ERROR(get_write_func(ep_context_config, &write_func, &write_state)); + } + return WriteEpContextDataWithFileFallback(api, write_func, write_state, file_name, fallback_file_name, graph, buffer, buffer_size); } diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc index 71f2dfac67d08..90ad4e7976824 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep.cc @@ -422,6 +422,9 @@ OrtStatus* ORT_API_CALL ExampleEp::CompileImpl(_In_ OrtEp* this_ptr, _In_ const std::string ep_cache_context; RETURN_IF_ERROR(ep_cache_context_attr.GetValue(ep_cache_context)); + // This example only exercises the load-side read flow (callback first, file fallback otherwise) to show how + // an EP retrieves EPContext binary data during compile. A real EP would consume `ep_context_data` (e.g., + // initialize a kernel/engine from it); here it is intentionally read and then discarded. std::vector ep_context_data; RETURN_IF_ERROR(ep_context_data_utils::ReadEpContextDataWithFileFallback( ep->ort_api, ep->ep_context_config_.get(), ep_cache_context.c_str(), ort_graphs[0], diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc index c0763b2f4db81..274876df53e7b 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc @@ -242,7 +242,7 @@ OrtStatus* ORT_API_CALL ExampleEpFactory::CreateEpImpl(OrtEpFactory* this_ptr, try { auto dummy_ep = std::make_unique( *factory, factory->ep_name_, config, *logger, - Ort::Experimental::EpContextConfig{session_options}); + Ort::Experimental::EpContextConfig{Ort::ConstSessionOptions{session_options}}); *ep = dummy_ep.release(); } catch (const Ort::Exception& e) { return factory->ort_api.CreateStatus(e.GetOrtErrorCode(), e.what()); diff --git a/onnxruntime/test/framework/ep_plugin_provider_test.cc b/onnxruntime/test/framework/ep_plugin_provider_test.cc index 1af90adcae26e..df1be371b3a29 100644 --- a/onnxruntime/test/framework/ep_plugin_provider_test.cc +++ b/onnxruntime/test/framework/ep_plugin_provider_test.cc @@ -23,9 +23,7 @@ #include "core/graph/model.h" #include "core/optimizer/graph_optimizer_registry.h" #include "core/session/abi_devices.h" -#include "core/session/model_compilation_options.h" #include "core/session/onnxruntime_cxx_api.h" -#include "core/session/onnxruntime_experimental_cxx_api.h" #include "core/session/onnxruntime_session_options_config_keys.h" #include "test/util/include/api_asserts.h" #include "test/util/include/asserts.h" @@ -61,28 +59,6 @@ static void CheckFileIsEmpty(const PathString& filename) { EXPECT_TRUE(content.empty()); } -struct EpContextWriteCallbackState { - bool called = false; - std::string file_name; - std::vector payload; -}; - -static OrtStatus* ORT_API_CALL EpContextWriteCallback(void* state, const char* file_name, const void* buffer, - size_t buffer_size) { - auto* write_state = static_cast(state); - write_state->called = true; - write_state->file_name = file_name; - write_state->payload.clear(); - if (buffer_size != 0) { - if (buffer == nullptr) { - return Ort::GetApi().CreateStatus(ORT_INVALID_ARGUMENT, - "EpContextWriteCallback received a null buffer for non-empty data"); - } - write_state->payload.assign(static_cast(buffer), static_cast(buffer) + buffer_size); - } - return nullptr; -} - // Normally, a plugin EP would be implemented in a separate library. // The `test_plugin_ep` namespace contains a local implementation intended for unit testing. namespace test_plugin_ep { @@ -1743,96 +1719,6 @@ TEST(PluginExecutionProviderTest, GetGraphCaptureNodeAssignmentPolicy) { } } -#if !defined(ORT_MINIMAL_BUILD) -// These framework tests intentionally inspect the internal session options held by ModelCompilationOptions. The pure -// public EPContext data API tests live in test/shared_lib/test_ep_context_data_api.cc. -TEST(PluginExecutionProviderTest, EpContextDataWriteFuncIsReturnedByEpApi) { - const auto& ort_api = Ort::GetApi(); - Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "EpContextDataWriteFuncIsReturnedByEpApi"}; - Ort::SessionOptions session_options; - Ort::ModelCompilationOptions compilation_options{env, session_options}; - - auto* set_write_func = - Ort::Experimental::Get_OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28_FnOrThrow( - &ort_api); - auto* get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_FnOrThrow(&ort_api); - auto* release_config_func = - Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_FnOrThrow(&ort_api); - auto* get_write_func = - Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_FnOrThrow(&ort_api); - - EpContextWriteCallbackState callback_state{}; - ASSERT_ORTSTATUS_OK(set_write_func(compilation_options, EpContextWriteCallback, &callback_state)); - - const auto* internal_options = reinterpret_cast( - static_cast(compilation_options)); - - OrtEpContextConfig* ep_context_config = nullptr; - ASSERT_ORTSTATUS_OK(get_config(&internal_options->GetSessionOptions(), &ep_context_config)); - auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); - - OrtWriteNamedBufferFunc write_func = nullptr; - void* callback_state_out = nullptr; - ASSERT_ORTSTATUS_OK(get_write_func(ep_context_config, &write_func, &callback_state_out)); - ASSERT_EQ(write_func, EpContextWriteCallback); - ASSERT_EQ(callback_state_out, &callback_state); - - const std::vector payload{'b', 'i', 'n', 'a', 'r', 'y'}; - ASSERT_ORTSTATUS_OK(write_func(callback_state_out, "engine.bin", payload.data(), payload.size())); - - ASSERT_TRUE(callback_state.called); - EXPECT_EQ(callback_state.file_name, "engine.bin"); - EXPECT_EQ(callback_state.payload, payload); -} - -TEST(PluginExecutionProviderTest, EpContextDataWriteFuncCanBeUsedWithEpContextBinaryInformation) { - const auto& ort_api = Ort::GetApi(); - Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "EpContextDataWriteFuncCanBeUsedWithEpContextBinaryInformation"}; - Ort::SessionOptions session_options; - Ort::ModelCompilationOptions compilation_options{env, session_options}; - - auto* set_write_func = - Ort::Experimental::Get_OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28_FnOrThrow( - &ort_api); - auto* get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_FnOrThrow(&ort_api); - auto* release_config_func = - Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_FnOrThrow(&ort_api); - auto* get_write_func = - Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_FnOrThrow(&ort_api); - - ASSERT_NO_THROW(compilation_options.SetEpContextBinaryInformation(ORT_TSTR("ep_context_dir/"), - ORT_TSTR("compiled_model.onnx"))); - - EpContextWriteCallbackState callback_state{}; - ASSERT_ORTSTATUS_OK(set_write_func(compilation_options, EpContextWriteCallback, &callback_state)); - - const auto* internal_options = reinterpret_cast( - static_cast(compilation_options)); - const auto& internal_session_options = internal_options->GetSessionOptions(); - - std::string ep_context_file_path; - ASSERT_TRUE(internal_session_options.value.config_options.TryGetConfigEntry(kOrtSessionOptionEpContextFilePath, - ep_context_file_path)); - EXPECT_THAT(ep_context_file_path, ::testing::HasSubstr("compiled_model.onnx")); - - OrtEpContextConfig* ep_context_config = nullptr; - ASSERT_ORTSTATUS_OK(get_config(&internal_session_options, &ep_context_config)); - auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); - - OrtWriteNamedBufferFunc write_func = nullptr; - void* callback_state_out = nullptr; - ASSERT_ORTSTATUS_OK(get_write_func(ep_context_config, &write_func, &callback_state_out)); - ASSERT_EQ(write_func, EpContextWriteCallback); - ASSERT_EQ(callback_state_out, &callback_state); - - const std::vector payload{'c', 't', 'x'}; - ASSERT_ORTSTATUS_OK(write_func(callback_state_out, "logical_context.bin", payload.data(), payload.size())); - EXPECT_TRUE(callback_state.called); - EXPECT_EQ(callback_state.file_name, "logical_context.bin"); - EXPECT_EQ(callback_state.payload, payload); -} -#endif // !defined(ORT_MINIMAL_BUILD) - // Helper: create a no-threshold resource accountant via the real factory (config ","). static IResourceAccountant* CreateNoThresholdAccountant(std::optional& acc_map) { ConfigOptions config; diff --git a/onnxruntime/test/shared_lib/test_ep_context_data_api.cc b/onnxruntime/test/shared_lib/test_ep_context_data_api.cc index 3656b955405cb..fc78f9c366777 100644 --- a/onnxruntime/test/shared_lib/test_ep_context_data_api.cc +++ b/onnxruntime/test/shared_lib/test_ep_context_data_api.cc @@ -167,8 +167,8 @@ TEST(EpContextDataApiTest, ApiRejectsInvalidArguments) { &ort_api); ExpectFailureOrtStatus(set_write_func(nullptr, EpContextWriteCallback, nullptr), ORT_INVALID_ARGUMENT, "OrtModelCompilationOptions is NULL"); - ExpectFailureOrtStatus(set_write_func(compilation_options, nullptr, nullptr), ORT_INVALID_ARGUMENT, - "OrtWriteNamedBufferFunc function is NULL"); + // A null write_func is allowed: it clears any previously set callback (covered by WriteFuncCanBeCleared), so it is + // not rejected here. #endif // !defined(ORT_MINIMAL_BUILD) } @@ -284,6 +284,50 @@ TEST(EpContextDataApiTest, WriteFuncCanBeSetOnModelCompilationOptions) { EXPECT_EQ(callback_state.file_name, "engine.bin"); EXPECT_EQ(callback_state.payload, payload); } + +TEST(EpContextDataApiTest, WriteFuncCanBeCleared) { + const auto& ort_api = Ort::GetApi(); + Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "EpContextDataWriteFuncCanBeCleared"}; + Ort::SessionOptions session_options; + Ort::ModelCompilationOptions compilation_options{env, session_options}; + + auto* set_write_func = + Ort::Experimental::Get_OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28_FnOrThrow( + &ort_api); + + EpContextWriteCallbackState callback_state{}; + ASSERT_ORTSTATUS_OK(set_write_func(compilation_options, EpContextWriteCallback, &callback_state)); + + // A null write_func clears the previously set callback (symmetric with the read setter) and must be accepted + // rather than rejected with ORT_INVALID_ARGUMENT. + ASSERT_ORTSTATUS_OK(set_write_func(compilation_options, nullptr, &callback_state)); +} + +TEST(EpContextDataApiTest, WriteFuncCanBeUsedWithEpContextBinaryInformation) { + const auto& ort_api = Ort::GetApi(); + Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "EpContextDataWriteFuncCanBeUsedWithEpContextBinaryInformation"}; + Ort::SessionOptions session_options; + Ort::ModelCompilationOptions compilation_options{env, session_options}; + + auto* set_write_func = + Ort::Experimental::Get_OrtCompileApi_ModelCompilationOptions_SetEpContextDataWriteFunc_SinceV28_FnOrThrow( + &ort_api); + + // The EPContext write callback and the EPContext binary information may be configured together; neither call + // rejects the other. + ASSERT_NO_THROW(compilation_options.SetEpContextBinaryInformation(ORT_TSTR("ep_context_dir/"), + ORT_TSTR("compiled_model.onnx"))); + + EpContextWriteCallbackState callback_state{}; + ASSERT_ORTSTATUS_OK(set_write_func(compilation_options, EpContextWriteCallback, &callback_state)); + + const std::vector payload{'c', 't', 'x'}; + ASSERT_ORTSTATUS_OK(EpContextWriteCallback(&callback_state, "logical_context.bin", payload.data(), payload.size())); + + ASSERT_TRUE(callback_state.called); + EXPECT_EQ(callback_state.file_name, "logical_context.bin"); + EXPECT_EQ(callback_state.payload, payload); +} #endif // !defined(ORT_MINIMAL_BUILD) TEST(EpContextDataApiTest, ReturnedReadFuncAllowsEmptyPayloads) { From 6e82dca7ac9d6380135287bf832a61425001871b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Jun 2026 21:49:46 +0000 Subject: [PATCH 44/48] Fix AndroidBinarySizeCheckJob_MinimalBaseline: bump threshold to 1438720 bytes --- .github/workflows/android.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 252ea7281d981..954c3313faf25 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -78,8 +78,8 @@ jobs: run: | set -e -x BINARY_SIZE_THRESHOLD_ARGS="" - echo "Binary size threshold in bytes: 1436672" - BINARY_SIZE_THRESHOLD_ARGS="--threshold_size_in_bytes 1436672" + echo "Binary size threshold in bytes: 1438720" + BINARY_SIZE_THRESHOLD_ARGS="--threshold_size_in_bytes 1438720" # Ensure ANDROID_NDK_HOME is available and get its real path if [ -z "$ANDROID_NDK_HOME" ]; then From 4fe683eae753f901f6de316fa71e84721f16bade Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Thu, 25 Jun 2026 14:52:16 -0700 Subject: [PATCH 45/48] Restore include for std::memset in ep_plugin_provider_test.cc --- onnxruntime/test/framework/ep_plugin_provider_test.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/onnxruntime/test/framework/ep_plugin_provider_test.cc b/onnxruntime/test/framework/ep_plugin_provider_test.cc index df1be371b3a29..e02105970e58b 100644 --- a/onnxruntime/test/framework/ep_plugin_provider_test.cc +++ b/onnxruntime/test/framework/ep_plugin_provider_test.cc @@ -5,6 +5,7 @@ #include #include +#include #include #include #include From b2e31a2618d85972bb9fb832e6314fa39ca41462 Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Thu, 25 Jun 2026 16:13:46 -0700 Subject: [PATCH 46/48] example EP: use EXCEPTION_TO_RETURNED_STATUS macros in CreateEpImpl Replaces the manual Ort::Exception try/catch in ExampleEpFactory::CreateEpImpl with the EXCEPTION_TO_RETURNED_STATUS_BEGIN/END macros (consistent with the example EP kernels), which also map std::exception and unknown exceptions to a returned OrtStatus to match the noexcept contract. --- .../library/example_plugin_ep/ep_factory.cc | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc index 274876df53e7b..a323ec0e8c15e 100644 --- a/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc +++ b/onnxruntime/test/autoep/library/example_plugin_ep/ep_factory.cc @@ -196,6 +196,7 @@ OrtStatus* ORT_API_CALL ExampleEpFactory::CreateEpImpl(OrtEpFactory* this_ptr, const OrtSessionOptions* session_options, const OrtLogger* logger, OrtEp** ep) noexcept { + EXCEPTION_TO_RETURNED_STATUS_BEGIN auto* factory = static_cast(this_ptr); *ep = nullptr; @@ -238,16 +239,14 @@ OrtStatus* ORT_API_CALL ExampleEpFactory::CreateEpImpl(OrtEpFactory* this_ptr, config.enable_weightless_ep_context_nodes = weightless_ep_context_nodes_enable == "1"; // The EpContextConfig wrapper extracts the EPContext callbacks from the session options and owns the handle. It - // throws if the experimental functions are unavailable or extraction fails, so convert that to an OrtStatus here. - try { - auto dummy_ep = std::make_unique( - *factory, factory->ep_name_, config, *logger, - Ort::Experimental::EpContextConfig{Ort::ConstSessionOptions{session_options}}); - *ep = dummy_ep.release(); - } catch (const Ort::Exception& e) { - return factory->ort_api.CreateStatus(e.GetOrtErrorCode(), e.what()); - } + // throws if the experimental functions are unavailable or extraction fails; EXCEPTION_TO_RETURNED_STATUS_END + // converts that (and any other exception thrown in this function) into an OrtStatus. + auto dummy_ep = std::make_unique( + *factory, factory->ep_name_, config, *logger, + Ort::Experimental::EpContextConfig{Ort::ConstSessionOptions{session_options}}); + *ep = dummy_ep.release(); return nullptr; + EXCEPTION_TO_RETURNED_STATUS_END } /*static*/ From e7f51d1addb89fde3297367ca29eb0f3567c42fc Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Thu, 25 Jun 2026 16:30:20 -0700 Subject: [PATCH 47/48] example EP helpers: reject model-derived EPContext names that designate a directory ValidateEpContextDataName and the model-derived (untrusted) branch of ResolveEpContextDataPath now reject a name whose final component is empty (a trailing separator) or is a dot, which would otherwise resolve to a directory and only surface later as a confusing file I/O failure. Adds an IsDirectoryOrEmptyName helper and covering tests. --- .../test/autoep/ep_context_data_utils_test.cc | 12 ++++++++++++ .../test/autoep/library/ep_context_data_utils.h | 17 +++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/onnxruntime/test/autoep/ep_context_data_utils_test.cc b/onnxruntime/test/autoep/ep_context_data_utils_test.cc index 6c9613a8f0b5d..8425280e4845b 100644 --- a/onnxruntime/test/autoep/ep_context_data_utils_test.cc +++ b/onnxruntime/test/autoep/ep_context_data_utils_test.cc @@ -161,6 +161,18 @@ TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathRejectsUnsafeNames) { ExpectOrtStatusError(ep_context_data_utils::ResolveEpContextDataPath(api, "../escape.ctx", empty_model_path_graph.ToExternal(), data_path), ORT_INVALID_ARGUMENT, "requires a model path"); + + // A model-derived name that designates a directory ("." or a trailing separator with an empty filename) is + // rejected up front, rather than resolving to a directory and failing later with a confusing I/O error. + ExpectOrtStatusError(ep_context_data_utils::ResolveEpContextDataPath(api, ".", empty_model_path_graph.ToExternal(), + data_path), + ORT_INVALID_ARGUMENT, "must refer to a file"); + ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback( + api, nullptr, ".", "unused.ctx", nullptr, nullptr, 0), + ORT_INVALID_ARGUMENT, "must refer to a file"); + ExpectOrtStatusError(ep_context_data_utils::WriteEpContextDataWithFileFallback( + api, nullptr, "sub/", "unused.ctx", nullptr, nullptr, 0), + ORT_INVALID_ARGUMENT, "must refer to a file"); } TEST(OrtEpLibrary, EpContextDataUtils_ResolvePathRejectsSymlinkEscape) { diff --git a/onnxruntime/test/autoep/library/ep_context_data_utils.h b/onnxruntime/test/autoep/library/ep_context_data_utils.h index 646f74634a4ae..7af35e3b75105 100644 --- a/onnxruntime/test/autoep/library/ep_context_data_utils.h +++ b/onnxruntime/test/autoep/library/ep_context_data_utils.h @@ -162,6 +162,15 @@ inline bool HasAbsoluteOrRootedPath(const std::filesystem::path& path) { return path.is_absolute() || path.has_root_name() || path.has_root_directory(); } +// Returns true if the final component of `path` is empty (e.g., a trailing separator like "sub/") or is the +// current-directory entry ".", i.e. the name designates a directory rather than a file (".." is handled separately by +// ContainsPathTraversal()). Such a name resolves to a directory and would only surface later as a confusing file I/O +// failure, so model-derived names like these are rejected up front. +inline bool IsDirectoryOrEmptyName(const std::filesystem::path& path) { + const std::filesystem::path leaf = path.filename(); + return leaf.empty() || leaf == std::filesystem::path{"."}; +} + // Returns true if `candidate_full` (a base-relative name already combined with `base`) resolves to a location inside // `base`. Both are normalized with std::filesystem::weakly_canonical, which resolves "." / ".." and any symlinks in // the existing portion of the path, so a name that escapes `base` directly or through a symlink is rejected. On @@ -209,6 +218,10 @@ inline OrtStatus* ValidateEpContextDataName(const OrtApi& api, const char* file_ return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not contain path traversal"); } + if (IsDirectoryOrEmptyName(candidate_path)) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must refer to a file, not a directory"); + } + data_name = candidate_path; return nullptr; } @@ -253,6 +266,10 @@ inline OrtStatus* ResolveEpContextDataPath(const OrtApi& api, const char* file_n return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must not be absolute or rooted"); } + if (IsDirectoryOrEmptyName(candidate_path)) { + return api.CreateStatus(ORT_INVALID_ARGUMENT, "EPContext data file name must refer to a file, not a directory"); + } + const ORTCHAR_T* model_path = nullptr; RETURN_IF_ERROR(api.Graph_GetModelPath(graph, &model_path)); if (model_path == nullptr || model_path[0] == 0) { From a3cd1baca73c1af442df0de907134238b0ad374a Mon Sep 17 00:00:00 2001 From: Gopalakrishnan Nallasamy Date: Thu, 25 Jun 2026 16:43:50 -0700 Subject: [PATCH 48/48] Address EPContext review nits Reword the OrtEpContextConfig definition comment (it is not opaque at the definition site); make ReadEpContextDataWithFileFallback exception-free by using the C allocator API (GetAllocatorWithDefaultOptions / AllocatorFree) instead of Ort::AllocatorWithDefaultOptions; alphabetize includes in ep_plugin_provider_test.cc; and use the Ort::Experimental::EpContextConfig RAII wrapper in the read-back tests in test_ep_context_data_api.cc. --- .../core/session/experimental_c_api.cc | 5 +- .../autoep/library/ep_context_data_utils.h | 11 +++- .../test/framework/ep_plugin_provider_test.cc | 4 +- .../shared_lib/test_ep_context_data_api.cc | 58 ++++--------------- 4 files changed, 24 insertions(+), 54 deletions(-) diff --git a/onnxruntime/core/session/experimental_c_api.cc b/onnxruntime/core/session/experimental_c_api.cc index a17ca4414ff4e..5b1a8460d1e1f 100644 --- a/onnxruntime/core/session/experimental_c_api.cc +++ b/onnxruntime/core/session/experimental_c_api.cc @@ -20,8 +20,9 @@ #include "core/session/model_compilation_options.h" #endif // !defined(ORT_MINIMAL_BUILD) -// Opaque handle backing the experimental OrtEpApi_* EPContext data functions. Holds copies of the application's -// EPContext read/write callbacks and opaque state extracted from an OrtSessionOptions instance. +// Backing definition of the OrtEpContextConfig handle used by the experimental OrtEpApi_* EPContext data functions. +// Holds copies of the application's EPContext read/write callbacks and opaque state extracted from an +// OrtSessionOptions instance. struct OrtEpContextConfig { OrtWriteNamedBufferFunc write_func = nullptr; void* write_state = nullptr; diff --git a/onnxruntime/test/autoep/library/ep_context_data_utils.h b/onnxruntime/test/autoep/library/ep_context_data_utils.h index 7af35e3b75105..a3f9a377ee92f 100644 --- a/onnxruntime/test/autoep/library/ep_context_data_utils.h +++ b/onnxruntime/test/autoep/library/ep_context_data_utils.h @@ -364,13 +364,18 @@ inline OrtStatus* ReadEpContextDataWithFileFallback( return ReadEpContextDataFromFile(api, file_name, graph, data); } - Ort::AllocatorWithDefaultOptions allocator; + // Use the C allocator API (not Ort::AllocatorWithDefaultOptions, whose constructor throws) so this OrtStatus*-based + // helper stays exception-free. The default allocator is owned by ORT and must not be released here. + OrtAllocator* allocator = nullptr; + RETURN_IF_ERROR(api.GetAllocatorWithDefaultOptions(&allocator)); void* ep_context_data = nullptr; size_t ep_context_data_size = 0; OrtStatus* status = read_func(read_state, file_name, allocator, &ep_context_data, &ep_context_data_size); - auto buffer_deleter = [&allocator](void* buffer_to_free) { + auto buffer_deleter = [&api, allocator](void* buffer_to_free) { if (buffer_to_free != nullptr) { - allocator.Free(buffer_to_free); + // Best-effort free during cleanup; release any returned status without throwing. + Ort::Status free_status{api.AllocatorFree(allocator, buffer_to_free)}; + static_cast(free_status); } }; std::unique_ptr ep_context_data_guard(ep_context_data, buffer_deleter); diff --git a/onnxruntime/test/framework/ep_plugin_provider_test.cc b/onnxruntime/test/framework/ep_plugin_provider_test.cc index e02105970e58b..4820f4e5c8898 100644 --- a/onnxruntime/test/framework/ep_plugin_provider_test.cc +++ b/onnxruntime/test/framework/ep_plugin_provider_test.cc @@ -6,15 +6,15 @@ #include #include #include -#include #include +#include #include #include #include "gsl/gsl" #include "gtest/gtest.h" -#include "core/common/path_string.h" #include "core/common/logging/sinks/file_sink.h" +#include "core/common/path_string.h" #include "core/framework/config_options.h" #include "core/framework/kernel_def_builder.h" #include "core/framework/op_kernel.h" diff --git a/onnxruntime/test/shared_lib/test_ep_context_data_api.cc b/onnxruntime/test/shared_lib/test_ep_context_data_api.cc index fc78f9c366777..ec8107f92aa7a 100644 --- a/onnxruntime/test/shared_lib/test_ep_context_data_api.cc +++ b/onnxruntime/test/shared_lib/test_ep_context_data_api.cc @@ -173,30 +173,19 @@ TEST(EpContextDataApiTest, ApiRejectsInvalidArguments) { } TEST(EpContextDataApiTest, AccessorsReturnNullWhenCallbacksUnset) { - const auto& ort_api = Ort::GetApi(); Ort::SessionOptions session_options; - - auto* get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_FnOrThrow(&ort_api); - auto* release_config_func = - Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_FnOrThrow(&ort_api); - auto* get_read_func = - Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_FnOrThrow(&ort_api); - auto* get_write_func = - Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_FnOrThrow(&ort_api); - - OrtEpContextConfig* ep_context_config = nullptr; - ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); - auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); + Ort::Experimental::EpContextConfig ep_context_config{session_options}; OrtReadNamedBufferFunc read_func = EpContextReadCallback; OrtWriteNamedBufferFunc write_func = EpContextWriteCallback; void* state = reinterpret_cast(0x1); - ASSERT_ORTSTATUS_OK(get_read_func(ep_context_config, &read_func, &state)); + ep_context_config.GetReadFunc(read_func, state); EXPECT_EQ(read_func, nullptr); EXPECT_EQ(state, nullptr); - ASSERT_ORTSTATUS_OK(get_write_func(ep_context_config, &write_func, &state)); + state = reinterpret_cast(0x1); + ep_context_config.GetWriteFunc(write_func, state); EXPECT_EQ(write_func, nullptr); EXPECT_EQ(state, nullptr); } @@ -207,30 +196,21 @@ TEST(EpContextDataApiTest, ConfigReturnsConfiguredCallbacks) { auto* set_read_func = Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_FnOrThrow(&ort_api); - auto* get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_FnOrThrow(&ort_api); - auto* release_config_func = - Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_FnOrThrow(&ort_api); - auto* get_read_func = - Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_FnOrThrow(&ort_api); - auto* get_write_func = - Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataWriteFunc_SinceV28_FnOrThrow(&ort_api); EpContextReadCallbackState callback_state{}; ASSERT_ORTSTATUS_OK(set_read_func(session_options, EpContextReadCallback, &callback_state)); - OrtEpContextConfig* ep_context_config = nullptr; - ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); - auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); + Ort::Experimental::EpContextConfig ep_context_config{session_options}; OrtReadNamedBufferFunc read_func = nullptr; void* read_state = nullptr; - ASSERT_ORTSTATUS_OK(get_read_func(ep_context_config, &read_func, &read_state)); + ep_context_config.GetReadFunc(read_func, read_state); EXPECT_EQ(read_func, EpContextReadCallback); EXPECT_EQ(read_state, &callback_state); OrtWriteNamedBufferFunc write_func = nullptr; void* write_state = nullptr; - ASSERT_ORTSTATUS_OK(get_write_func(ep_context_config, &write_func, &write_state)); + ep_context_config.GetWriteFunc(write_func, write_state); EXPECT_EQ(write_func, nullptr); EXPECT_EQ(write_state, nullptr); } @@ -241,24 +221,16 @@ TEST(EpContextDataApiTest, ReadFuncCanBeCleared) { auto* set_read_func = Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_FnOrThrow(&ort_api); - auto* get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_FnOrThrow(&ort_api); - auto* release_config_func = - Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_FnOrThrow(&ort_api); - auto* get_read_func = - Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_FnOrThrow(&ort_api); EpContextReadCallbackState callback_state{}; ASSERT_ORTSTATUS_OK(set_read_func(session_options, EpContextReadCallback, &callback_state)); ASSERT_ORTSTATUS_OK(set_read_func(session_options, nullptr, &callback_state)); - OrtEpContextConfig* ep_context_config = nullptr; - ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); - auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); - + Ort::Experimental::EpContextConfig ep_context_config{session_options}; OrtReadNamedBufferFunc read_func = EpContextReadCallback; void* read_state = reinterpret_cast(0x1); - ASSERT_ORTSTATUS_OK(get_read_func(ep_context_config, &read_func, &read_state)); + ep_context_config.GetReadFunc(read_func, read_state); EXPECT_EQ(read_func, nullptr); EXPECT_EQ(read_state, nullptr); } @@ -336,22 +308,14 @@ TEST(EpContextDataApiTest, ReturnedReadFuncAllowsEmptyPayloads) { auto* set_read_func = Ort::Experimental::Get_OrtApi_SessionOptions_SetEpContextDataReadFunc_SinceV28_FnOrThrow(&ort_api); - auto* get_config = Ort::Experimental::Get_OrtEpApi_SessionOptions_GetEpContextConfig_SinceV28_FnOrThrow(&ort_api); - auto* release_config_func = - Ort::Experimental::Get_OrtEpApi_ReleaseEpContextConfig_SinceV28_FnOrThrow(&ort_api); - auto* get_read_func = - Ort::Experimental::Get_OrtEpApi_EpContextConfig_GetEpContextDataReadFunc_SinceV28_FnOrThrow(&ort_api); EpContextReadCallbackState callback_state{}; ASSERT_ORTSTATUS_OK(set_read_func(session_options, EpContextReadCallback, &callback_state)); - OrtEpContextConfig* ep_context_config = nullptr; - ASSERT_ORTSTATUS_OK(get_config(session_options, &ep_context_config)); - auto release_config = gsl::finally([&]() { release_config_func(ep_context_config); }); - + Ort::Experimental::EpContextConfig ep_context_config{session_options}; OrtReadNamedBufferFunc read_func = nullptr; void* read_state = nullptr; - ASSERT_ORTSTATUS_OK(get_read_func(ep_context_config, &read_func, &read_state)); + ep_context_config.GetReadFunc(read_func, read_state); ASSERT_EQ(read_func, EpContextReadCallback); ASSERT_EQ(read_state, &callback_state);