Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion include/onnxruntime/core/session/onnxruntime_c_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 10 additions & 5 deletions onnxruntime/core/session/custom_ops.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#include <algorithm>

#include <gsl/gsl>
#include "core/common/safeint.h"
Expand Down Expand Up @@ -916,21 +917,25 @@ 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<uint32_t>(ORT_API_VERSION));
Comment thread
chilo-ms marked this conversation as resolved.
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) {
op_kernel_ = nullptr;
Ort::ThrowOnError(
op_.CreateKernelV2(
&op_,
OrtGetApiBase()->GetApi(op_.version),
ort_api,
reinterpret_cast<const OrtKernelInfo*>(&info),
&op_kernel_));
} else {
op_kernel_ = op_.CreateKernel(&op_, OrtGetApiBase()->GetApi(op_.version),
op_kernel_ = op_.CreateKernel(&op_, ort_api,
reinterpret_cast<const OrtKernelInfo*>(&info));
}
}
Expand Down
30 changes: 30 additions & 0 deletions onnxruntime/test/shared_lib/test_inference.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<Input<float>> 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<int64_t> expected_dims_y = {3, 2};
std::vector<float> 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<float>(*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) {
Expand Down
Loading