What Are ONNX Runtime Contrib Operators and How to Register Custom Ones?

Contrib operators are ONNX Runtime-specific extensions in the Microsoft domain (kMSDomain) that extend beyond the ONNX specification, while custom operators are user-defined kernels that you register at runtime via the Ort::CustomOpDomain API.

ONNX Runtime ships with hundreds of built-in operators, but real-world models often require functionality outside the official ONNX specification. In the microsoft/onnxruntime repository, contrib operators provide Microsoft-supported extensions maintained in the com.microsoft domain, while the custom operator API allows you to inject your own kernels at runtime without recompiling the framework.

Understanding Contrib Operators in ONNX Runtime

Contrib operators are implementation-specific extensions that reside in the Microsoft domain (kMSDomain) rather than the standard ONNX namespace. These operators are maintained by Microsoft and partners to expose bleeding-edge functionality before it becomes part of the official ONNX spec.

According to the source code in onnxruntime/core/graph/contrib_ops/contrib_defs.h, contrib operator schemas are registered globally by the RegisterContribSchemas() function implemented in onnxruntime/core/graph/contrib_ops/contrib_defs.cc. The concrete kernel implementations are grouped by execution provider in files such as onnxruntime/contrib_ops/cpu/cpu_contrib_kernels.cc, where each provider calls Register*ContribKernels() to bind schemas to implementations.

To use a built-in contrib op, reference it in your model with the domain string com.microsoft (defined internally as kMSDomain). Common examples include LayerNormalization and Range, which are registered alongside their shape-inference logic in the contrib schema registry.

How to Register Custom Operators in ONNX Runtime

When you need functionality that contrib operators do not provide, you can register custom operators using the C++ or C API. The registration flow follows the same pattern as internal operators but uses the public OrtCustomOpDomain APIs instead of the internal registry.

Step 1: Define the Operator Schema and Kernel

Create a struct that inherits from Ort::CustomOpBase. You must implement virtual methods to declare the operator's name, input/output counts, and data types. The test harness in onnxruntime/test/shared_lib/custom_op_utils.h provides the canonical pattern for this.

struct MyAddOp : Ort::CustomOpBase<MyAddOp, MyAddKernel> {
  void* CreateKernel(const OrtApi& api, const OrtKernelInfo*) const { 
    return new MyAddKernel(api); 
  }
  
  const char* GetName() const noexcept { return "MyAdd"; }

  size_t GetInputTypeCount() const noexcept { return 2; }
  ONNXTensorElementDataType GetInputType(size_t) const noexcept { 
    return ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT; 
  }

  size_t GetOutputTypeCount() const noexcept { return 1; }
  ONNXTensorElementDataType GetOutputType(size_t) const noexcept { 
    return ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT; 
  }
};

struct MyAddKernel {
  MyAddKernel(const OrtApi& api) : api_(api), ort_(api_) {}
  
  void Compute(OrtKernelContext* context) {
    const OrtValue* a = ort_.KernelContext_GetInput(context, 0);
    const OrtValue* b = ort_.KernelContext_GetInput(context, 1);
    // Perform element-wise addition, allocate output, fill it
  }
  
  const OrtApi& api_;
  Ort::CustomOpApi ort_;
};

Step 2: Create a Custom Domain

Instantiate an Ort::CustomOpDomain (or the C API OrtCustomOpDomain*) to namespace your operators. This prevents collisions with built-in operators and contrib operators.

Ort::CustomOpDomain custom_domain("my.custom.domain");

Step 3: Add the Operator to the Domain

Register your operator instance with the domain using Add(). The instance must outlive the session. In the C API, this corresponds to OrtApis::CustomOpDomain_Add.

MyAddOp my_add_op;
custom_domain.Add(&my_add_op);

Step 4: Attach the Domain to Session Options

Pass the domain to your OrtSessionOptions using the Add method. The C API equivalent is OrtApis::AddCustomOpDomain. When you create the session, ONNX Runtime will resolve any nodes with your custom domain and dispatch them to your kernel implementation.

Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "example");
Ort::SessionOptions session_options;
session_options.Add(custom_domain);

Ort::Session session(env, "model_with_myadd.onnx", session_options);

Alternative: Register via Shared Library (C API)

For dynamic loading, compile your operators into a shared library that exports a RegisterCustomOps function. Use OrtRegisterCustomOpsLibrary to load it at runtime, which internally uses CreateCustomRegistry from onnxruntime/core/session/custom_ops.h to bundle the domains.

OrtSessionOptions* opts;
OrtCreateSessionOptions(&opts);

/* Load a shared library that exports:
 *   OrtStatus* RegisterCustomOps(OrtSessionOptions* options, const OrtApiBase* api_base);
 */
OrtRegisterCustomOpsLibrary(opts, "/path/to/my_custom_ops.so", NULL);

OrtSession* sess;
OrtCreateSession(env, "model.onnx", opts, &sess);

Key Implementation Files in ONNX Runtime

The following files in microsoft/onnxruntime define the contrib and custom operator infrastructure:

Summary

  • Contrib operators are Microsoft-maintained extensions in the com.microsoft domain (kMSDomain), registered via RegisterContribSchemas() and implemented per execution provider in files like cpu_contrib_kernels.cc.
  • Custom operators are user-defined kernels that require implementing Ort::CustomOpBase, creating an Ort::CustomOpDomain, and attaching it to OrtSessionOptions before session creation.
  • The CreateCustomRegistry utility in custom_ops.h bundles custom domains for internal use by the Standalone Op Invoker when persisting schemas to ORT format.
  • Both registration methods allow ONNX Runtime to execute operators outside the standard ONNX specification, whether built-in or user-defined.

Frequently Asked Questions

What is the difference between contrib operators and custom operators?

Contrib operators are built into ONNX Runtime and maintained in the Microsoft domain (kMSDomain), with schemas defined in contrib_defs.cc and kernels in provider-specific files like cpu_contrib_kernels.cc. Custom operators are defined by users at runtime through the Ort::CustomOpDomain API without modifying the ONNX Runtime source code, allowing you to extend functionality for specific models.

How do I load a custom operator library in Python?

Use the register_custom_ops_library method on onnxruntime.SessionOptions in the Python API. This wraps the underlying OrtRegisterCustomOpsLibrary C API call and loads your shared library containing the RegisterCustomOps export, making your custom ops available to the inference session.

Why are contrib operators placed in the com.microsoft domain?

The com.microsoft domain (internally kMSDomain) isolates Microsoft-specific extensions from the standard ONNX operator set, ensuring that models using these ops are explicitly marked as requiring ONNX Runtime rather than being portable to all ONNX-compliant frameworks. This distinction is enforced by the schema registration logic in contrib_defs.cc.

Can custom operators be used with any execution provider?

Yes. Once registered via OrtSessionOptions, custom operators are available to all execution providers in the session. However, your kernel implementation must be compatible with the provider's execution context—CUDA kernels for the CUDA provider, CPU kernels for the CPU provider, etc.—as the runtime dispatches the operator to the appropriate kernel based on the provider configuration.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →