Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 this to the runtime API version, so custom ops
// compiled against a newer ORT can work on an older runtime if they only use APIs available
// at the runtime version. Individual OrtCustomOp function pointers added in later versions are
// gated by per-function version checks within ORT.
Comment thread
Copilot marked this conversation as resolved.
Outdated

// This callback creates the kernel, which is a user defined
// parameter that is passed to the Kernel* callbacks below. It is
Expand Down
13 changes: 8 additions & 5 deletions onnxruntime/core/session/custom_ops.cc
Original file line number Diff line number Diff line change
Expand Up @@ -916,21 +916,24 @@
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));

Check warning on line 922 in onnxruntime/core/session/custom_ops.cc

View workflow job for this annotation

GitHub Actions / Optional Lint C++

[cpplint] reported by reviewdog 🐶 Add #include <algorithm> for min [build/include_what_you_use] [4] Raw Output: onnxruntime/core/session/custom_ops.cc:922: Add #include <algorithm> for min [build/include_what_you_use] [4]
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);
Comment thread
chilo-ms marked this conversation as resolved.
Outdated

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
32 changes: 32 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,38 @@ 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 and executed 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).
Comment thread
chilo-ms marked this conversation as resolved.
#ifndef ABSL_HAVE_ADDRESS_SANITIZER
Comment thread
edgchen1 marked this conversation as resolved.
Outdated
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);
}
#endif

// Memory leak
#ifndef ABSL_HAVE_ADDRESS_SANITIZER
TEST(CApiTest, custom_op_handler) {
Expand Down
Loading