diff --git a/include/onnxruntime/core/session/onnxruntime_c_api.h b/include/onnxruntime/core/session/onnxruntime_c_api.h index a73868527771a..7a29ec34d5808 100644 --- a/include/onnxruntime/core/session/onnxruntime_c_api.h +++ b/include/onnxruntime/core/session/onnxruntime_c_api.h @@ -7545,7 +7545,10 @@ typedef enum OrtCustomOpInputOutputCharacteristic { * the implementor of the custom op. */ struct OrtCustomOp { - uint32_t version; // Must be initialized to ORT_API_VERSION + uint32_t version; // Initialize to ORT_API_VERSION. ORT will cap the API version used to select the OrtApi passed + // to CreateKernel/CreateKernelV2 callbacks, so custom ops compiled against a newer ORT can load + // on an older runtime if they only use APIs available at the runtime version. Newer OrtCustomOp + // function pointers are gated by per-function version checks within ORT. // This callback creates the kernel, which is a user defined // parameter that is passed to the Kernel* callbacks below. It is diff --git a/onnxruntime/core/session/custom_ops.cc b/onnxruntime/core/session/custom_ops.cc index 89969172c1bdc..e6ad2ed8481c9 100644 --- a/onnxruntime/core/session/custom_ops.cc +++ b/onnxruntime/core/session/custom_ops.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include "core/common/safeint.h" @@ -916,9 +917,13 @@ ORT_API_STATUS_IMPL(OrtApis::KernelContext_GetScratchBuffer, _In_ const OrtKerne namespace onnxruntime { struct CustomOpKernel : OpKernel { CustomOpKernel(const OpKernelInfo& info, const OrtCustomOp& op) : OpKernel(info), op_(op) { - if (op_.version > ORT_API_VERSION) { - ORT_THROW("Unsupported version '" + std::to_string(op_.version) + "' in custom op '" + op.GetName(&op)); - } + // Cap the version to the current ORT API version. This allows custom ops compiled against a newer ORT + // to work on an older ORT runtime, provided they don't call API functions unavailable at the runtime version. + // Individual newer functions in OrtCustomOp are gated by per-function version checks throughout this file. + const uint32_t api_version = std::min(op_.version, static_cast(ORT_API_VERSION)); + const OrtApi* ort_api = OrtGetApiBase()->GetApi(api_version); + ORT_ENFORCE(ort_api != nullptr, "Failed to get ORT API for version ", + api_version, " in custom op '", op_.GetName(&op_), "'"); if (op_.version >= min_ort_version_with_compute_v2_support && op_.CreateKernelV2) { @@ -926,11 +931,11 @@ struct CustomOpKernel : OpKernel { Ort::ThrowOnError( op_.CreateKernelV2( &op_, - OrtGetApiBase()->GetApi(op_.version), + ort_api, reinterpret_cast(&info), &op_kernel_)); } else { - op_kernel_ = op_.CreateKernel(&op_, OrtGetApiBase()->GetApi(op_.version), + op_kernel_ = op_.CreateKernel(&op_, ort_api, reinterpret_cast(&info)); } } diff --git a/onnxruntime/test/shared_lib/test_inference.cc b/onnxruntime/test/shared_lib/test_inference.cc index 809d1b903e8cd..a4147383b7c6a 100644 --- a/onnxruntime/test/shared_lib/test_inference.cc +++ b/onnxruntime/test/shared_lib/test_inference.cc @@ -748,6 +748,36 @@ TEST(CApiTest, SparseInputModel) { #endif // DISABLE_CONTRIB_OPS #endif // !defined(DISABLE_SPARSE_TENSORS) +// Test that a custom op compiled against a newer ORT version (higher OrtCustomOp::version) +// can still be loaded on this ORT runtime. This simulates the forward compatibility +// scenario where an IHV EP compiled against ORT v(N+1) is loaded into ORT v(N). +// We test session creation only (which covers schema registration, kernel def building, and +// CustomOpKernel construction with the capped API version). +TEST(CApiTest, custom_op_forward_version_compat) { + std::vector> inputs(1); + auto& input = inputs[0]; + input.name = "X"; + input.dims = {3, 2}; + input.values = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f}; + + std::vector expected_dims_y = {3, 2}; + std::vector expected_values_y = {2.0f, 4.0f, 6.0f, 8.0f, 10.0f, 12.0f}; + + MyCustomOp custom_op{onnxruntime::kCpuExecutionProvider}; + + // Simulate an EP compiled against a newer ORT version by setting version higher than ORT_API_VERSION. + // The OrtCustomOp struct's function pointers are still valid for the current version, so this should work. + custom_op.version = ORT_API_VERSION + 1; + + Ort::CustomOpDomain custom_op_domain("test"); + custom_op_domain.Add(&custom_op); + + // test_session_creation_only=true: exercises schema registration, kernel def building, and + // CustomOpKernel construction (which was previously blocked by the blanket version reject). + TestInference(*ort_env, CUSTOM_OP_MODEL_URI, inputs, "Y", expected_dims_y, expected_values_y, 0, + custom_op_domain, nullptr, /*test_session_creation_only=*/true); +} + // Memory leak #ifndef ABSL_HAVE_ADDRESS_SANITIZER TEST(CApiTest, custom_op_handler) {