Skip to content

Commit 1ed8fd9

Browse files
[EP ABI] Add APIs to create control flow kernels for plugin EPs (microsoft#26927)
### Description Adds APIs to enable plugin EPs to create and register kernels for control flow operators (If, Loop, and Scan). The implementation provides ORT-managed kernel implementations that handle subgraph execution while allowing EPs to provide device-specific helper functions for operations like tensor concatenation and transposition. Key changes: - Adds four EP API functions: `CreateIfKernel`, `CreateLoopKernel`, `CreateScanKernel`, and `ReleaseKernelImpl` for creating control flow kernel implementations - Introduces public helper structures (`OrtLoopKernelHelper`, `OrtScanKernelHelper`) that EPs implement to provide device-specific operations - Updates the example kernel-based EP with kernel registrations for all control flow operators and adds corresponding test models. ### Motivation and Context <!-- - Why is this change required? What problem does it solve? - If it fixes an open issue, please link to the issue here. -->
1 parent 39d8520 commit 1ed8fd9

31 files changed

Lines changed: 1384 additions & 29 deletions

cmake/onnxruntime_unittests.cmake

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2134,7 +2134,13 @@ if (onnxruntime_BUILD_SHARED_LIB AND
21342134
"${TEST_SRC_DIR}/autoep/library/example_plugin_ep_kernel_registry/kernels/relu.h"
21352135
"${TEST_SRC_DIR}/autoep/library/example_plugin_ep_kernel_registry/kernels/relu.cc"
21362136
"${TEST_SRC_DIR}/autoep/library/example_plugin_ep_kernel_registry/kernels/binary_op.h"
2137-
"${TEST_SRC_DIR}/autoep/library/example_plugin_ep_kernel_registry/kernels/binary_op.cc")
2137+
"${TEST_SRC_DIR}/autoep/library/example_plugin_ep_kernel_registry/kernels/binary_op.cc"
2138+
"${TEST_SRC_DIR}/autoep/library/example_plugin_ep_kernel_registry/kernels/scan.h"
2139+
"${TEST_SRC_DIR}/autoep/library/example_plugin_ep_kernel_registry/kernels/scan.cc"
2140+
"${TEST_SRC_DIR}/autoep/library/example_plugin_ep_kernel_registry/kernels/loop.h"
2141+
"${TEST_SRC_DIR}/autoep/library/example_plugin_ep_kernel_registry/kernels/loop.cc"
2142+
"${TEST_SRC_DIR}/autoep/library/example_plugin_ep_kernel_registry/kernels/if.h"
2143+
"${TEST_SRC_DIR}/autoep/library/example_plugin_ep_kernel_registry/kernels/if.cc")
21382144
onnxruntime_add_shared_library_module(example_plugin_ep_kernel_registry ${onnxruntime_autoep_test_example_plugin_ep_kernel_registry_src})
21392145
target_include_directories(example_plugin_ep_kernel_registry PRIVATE ${REPO_ROOT}/include/onnxruntime/core/session)
21402146
target_link_libraries(example_plugin_ep_kernel_registry PRIVATE onnxruntime ${GSL_TARGET})

include/onnxruntime/core/session/onnxruntime_ep_c_api.h

Lines changed: 192 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,8 +524,11 @@ typedef struct OrtKernelImpl OrtKernelImpl;
524524
*/
525525
struct OrtKernelImpl {
526526
uint32_t ort_version_supported; ///< Must be initialized to ORT_API_VERSION
527+
uint32_t flags; ///< EP must initialize to 0. Used internally by ORT.
527528

528529
/** \brief Computation function called to execute the kernel on an EP.
530+
*
531+
* \note Implementation of this function is required.
529532
*
530533
* \param[in] this_ptr The OrtKernelImpl instance.
531534
* \param[in] context The OrtKernelContext instance that provides access to the inputs and outputs.
@@ -537,6 +540,8 @@ struct OrtKernelImpl {
537540
ORT_API2_STATUS(Compute, _In_ OrtKernelImpl* this_ptr, _In_ OrtKernelContext* context);
538541

539542
/** \brief Called by ORT to release the OrtKernelImpl instance and its resources.
543+
*
544+
* \note Implementation of this function is required.
540545
*
541546
* \param[in] this_ptr The OrtKernelImpl instance.
542547
*
@@ -645,7 +650,9 @@ struct OrtKernelImpl {
645650
* \param[in] kernel_create_func_state Opaque state initially provided by the EP that registered the kernel.
646651
* Refer to OrtEpApi::KernelRegistry_AddKernel(). May be null.
647652
* \param[in] info The OrtKernelInfo instance that provides access to the kernel's input and output characteristics.
648-
* \param[out] kernel_out Output parameter set to the new OrtKernelImpl instance.
653+
* \param[out] kernel_out Output parameter set to the new OrtKernelImpl instance. On success, ownership of this
654+
* OrtKernelImpl instance transfers to ORT, which will call OrtKernelImpl::Release() to
655+
* release the instance when it is no longer used.
649656
*
650657
* \snippet{doc} snippets.dox OrtStatus Return Value
651658
*
@@ -655,6 +662,89 @@ typedef OrtStatus*(ORT_API_CALL* OrtKernelCreateFunc)(_In_ void* kernel_create_f
655662
_In_ const OrtKernelInfo* info,
656663
_Outptr_result_maybenull_ OrtKernelImpl** kernel_out);
657664

665+
struct OrtLoopKernelHelper;
666+
typedef struct OrtLoopKernelHelper OrtLoopKernelHelper;
667+
668+
/**
669+
* \brief Contains helper functions for a Loop OrtKernelImpl created via ::CreateLoopKernel().
670+
* \since Version 1.24.
671+
*/
672+
struct OrtLoopKernelHelper {
673+
uint32_t ort_version_supported; ///< Must be initialized to ORT_API_VERSION
674+
675+
/** \brief Called by ORT to release the OrtLoopKernelHelper instance and its resources.
676+
*
677+
* \param[in] this_ptr The OrtLoopKernelHelper instance.
678+
*
679+
* \since Version 1.24.
680+
*/
681+
ORT_API_T(void, Release, _In_ OrtLoopKernelHelper* this_ptr);
682+
683+
/** \brief Helper function that concatenates OrtValue instances from each loop iteration into a single
684+
* pre-allocated output buffer.
685+
*
686+
* \note Implementing this function is required for all Loop opset versions.
687+
*
688+
* \param[in] this_ptr The OrtLoopKernelHelper instance.
689+
* \param[in] stream_handle Optional native stream handle that enables asynchronous operations. May be NULL.
690+
* \param[in] per_iteration_outputs Array of OrtValue instances from each iteration. All OrtValue elements have the
691+
* same shape.
692+
* \param[in] num_per_iteration_outputs The number of OrtValue* elements in the `per_iteration_outputs` array.
693+
* \param[out] output The pre-allocated output buffer. Memory is allocated on the device for the EP running the
694+
* Loop node.
695+
* \param[in] output_size_in_bytes The size in bytes of the `output` buffer. It is guaranteed to be large enough
696+
* to hold the concatenated data of each element in `per_iteration_outputs`.
697+
*
698+
* \snippet{doc} snippets.dox OrtStatus Return Value
699+
*
700+
* \since Version 1.24.
701+
*/
702+
ORT_API2_STATUS(ConcatOutput, _In_ OrtLoopKernelHelper* this_ptr, _In_opt_ void* stream_handle,
703+
_In_reads_(num_per_iteration_outputs) const OrtValue* const* per_iteration_outputs,
704+
_In_ size_t num_per_iteration_outputs, _Out_writes_bytes_all_(output_size_in_bytes) void* output,
705+
_In_ size_t output_size_in_bytes);
706+
};
707+
708+
struct OrtScanKernelHelper;
709+
typedef struct OrtScanKernelHelper OrtScanKernelHelper;
710+
711+
/**
712+
* \brief Contains helper functions for a Scan OrtKernelImpl created via ::CreateScanKernel().
713+
* \since Version 1.24.
714+
*/
715+
struct OrtScanKernelHelper {
716+
uint32_t ort_version_supported; ///< Must be initialized to ORT_API_VERSION
717+
718+
/** \brief Called by ORT to release the OrtScanKernelHelper instance and its resources.
719+
*
720+
* \param[in] this_ptr The OrtScanKernelHelper instance.
721+
*
722+
* \since Version 1.24.
723+
*/
724+
ORT_API_T(void, Release, _In_ OrtScanKernelHelper* this_ptr);
725+
726+
/** \brief Helper function that transposes an OrtValue instance during execution of a Scan kernel.
727+
*
728+
* \note Called for Scan (opset >= 9) when the 'scan_input_axes' or 'scan_output_axes' attributes contain
729+
* non-zero values. Implementing this function is required for Scan opset versions >= 9.
730+
*
731+
* \param[in] this_ptr The OrtScanKernelHelper instance.
732+
* \param[in] permutation An array of integers that defines how the input tensor's axes should be permuted.
733+
* \param[in] num_permutation_elems The number of integer elements in the `permutation` array.
734+
* \param[in] input The input OrtValue tensor to transpose.
735+
* \param[in] stream An optional OrtSyncStream instance to be used for asynchronous operations. May be NULL.
736+
* \param[out] output The pre-allocated output OrtValue instance into which to store the results of the
737+
* transpose operation. Must not be released as it is owned by ORT.
738+
*
739+
* \snippet{doc} snippets.dox OrtStatus Return Value
740+
*
741+
* \since Version 1.24.
742+
*/
743+
ORT_API2_STATUS(Transpose, _In_ OrtScanKernelHelper* this_ptr,
744+
_In_reads_(num_permutation_elems) const size_t* permutation, _In_ size_t num_permutation_elems,
745+
_In_ const OrtValue* input, _In_opt_ OrtSyncStream* stream, _Inout_ OrtValue* output);
746+
};
747+
658748
/**
659749
* \brief The OrtEpApi struct provides functions that are relevant to the implementation of an execution provider.
660750
*
@@ -1217,6 +1307,107 @@ struct OrtEpApi {
12171307
* \since Version 1.24
12181308
*/
12191309
ORT_API2_STATUS(KernelInfo_GetEp, _In_ const OrtKernelInfo* info, _Outptr_ const OrtEp** ep);
1310+
1311+
/** \brief Creates an OrtKernelImpl instance for an If operator.
1312+
*
1313+
* Control flow operators require access to ORT session internals to orchestrate subgraph operations.
1314+
* This function allows an EP to create a properly configured OrtKernelImpl with access to ORT internals that
1315+
* the EP can add to its kernel registry.
1316+
*
1317+
* An EP is required to create an OrtKernelDef that keeps input[0] ('cond') on the CPU (i.e., OrtMemTypeCPUInput)
1318+
* as this input is used by CPU logic. The output should remain on the device (i.e., OrtMemTypeDefault), which is
1319+
* the default setting, to avoid copying to/from CPU.
1320+
*
1321+
* Example kernel definition (CXX API):
1322+
* Ort::KernelDef kernel_def = Ort::KernelDefBuilder()
1323+
* .SetDomain("").SetOperatorType("If").SetSinceVersion(21, 22)
1324+
* .SetExecutionProvider("MyEp")
1325+
* .SetInputMemType(0, OrtMemTypeCPUInput) // 'cond' on CPU
1326+
* .SetOutputMemType(0, OrtMemTypeDefault) // output on EP device
1327+
* .AddTypeConstraint("B", ...)
1328+
* .AddTypeConstraint("V", ...).Build();
1329+
*
1330+
* \param[in] kernel_info The ::OrtKernelInfo instance for an If node. This function returns error ORT_FAIL
1331+
* if the opset version specified by `kernel_info` is unsupported.
1332+
* \param[out] kernel_out Output parameter set to the OrtKernelImpl instance for the If node.
1333+
* Must be released via ::ReleaseKernelImpl, unless ownership is transferred
1334+
* to ORT (see OrtKernelCreateFunc and ::KernelRegistry_AddKernel()).
1335+
*
1336+
* \snippet{doc} snippets.dox OrtStatus Return Value
1337+
* \since Version 1.24
1338+
*/
1339+
ORT_API2_STATUS(CreateIfKernel, _In_ const OrtKernelInfo* kernel_info, _Outptr_ OrtKernelImpl** kernel_out);
1340+
1341+
/** \brief Creates an OrtKernelImpl instance for a Loop operator.
1342+
*
1343+
* Control flow operators require access to ORT session internals to orchestrate subgraph operations.
1344+
* This function allows an EP to create a properly configured OrtKernelImpl with access to ORT internals that
1345+
* the EP can add to its kernel registry.
1346+
*
1347+
* An EP is required to create an OrtKernelDef that keeps input[0] ('M') and input[1] ('cond') on the CPU
1348+
* (i.e., OrtMemTypeCPUInput) as these inputs are used by CPU logic. Input[2] ('v_initial') and the output should
1349+
* remain on the device (i.e., OrtMemTypeDefault), which is the default setting, to avoid copying to/from CPU.
1350+
*
1351+
* Example kernel definition (CXX API):
1352+
* Ort::KernelDef kernel_def = Ort::KernelDefBuilder()
1353+
* .SetDomain("").SetOperatorType("Loop").SetSinceVersion(21, 22)
1354+
* .SetExecutionProvider("MyEp")
1355+
* .SetInputMemType(0, OrtMemTypeCPUInput) // 'M' on CPU
1356+
* .SetInputMemType(1, OrtMemTypeCPUInput) // 'cond' on CPU
1357+
* .SetInputMemType(2, OrtMemTypeDefault) // 'v_initial' on EP device
1358+
* .SetOutputMemType(0, OrtMemTypeDefault) // output on EP device
1359+
* .AddTypeConstraint("I", ...)
1360+
* .AddTypeConstraint("B", ...)
1361+
* .AddTypeConstraint("V", ...).Build();
1362+
*
1363+
* \param[in] kernel_info The ::OrtKernelInfo instance for a Loop node. This function returns error ORT_FAIL
1364+
* if the opset version specified by `kernel_info` is unsupported.
1365+
* \param[in] helper A OrtLoopKernelHelper instance that contains helper functions that ORT calls during kernel
1366+
* execution to operate on tensors allocated with the EP's device memory.
1367+
* ORT will call OrtLoopKernelHelper::Release() to release the helper and its resources.
1368+
* \param[out] kernel_out Output parameter set to the OrtKernelImpl instance for the Loop node.
1369+
* Must be released via ::ReleaseKernelImpl, unless ownership is transferred
1370+
* to ORT (see OrtKernelCreateFunc and ::KernelRegistry_AddKernel()).
1371+
*
1372+
* \snippet{doc} snippets.dox OrtStatus Return Value
1373+
* \since Version 1.24
1374+
*/
1375+
ORT_API2_STATUS(CreateLoopKernel, _In_ const OrtKernelInfo* kernel_info, _In_ OrtLoopKernelHelper* helper,
1376+
_Outptr_ OrtKernelImpl** kernel_out);
1377+
1378+
/** \brief Creates an OrtKernelImpl instance for a Scan operator. Does not support opset versions older than 9.
1379+
*
1380+
* Control flow operators require access to ORT session internals to orchestrate subgraph operations.
1381+
* This function allows an EP to create a properly configured OrtKernelImpl with access to ORT internals that
1382+
* the EP can add to its kernel registry.
1383+
*
1384+
* It is recommended that an EP create an OrtKernelDef that keeps the inputs and outputs on the EP's
1385+
* device (i.e., OrtMemTypeDefault), which is the default setting, to avoid copying to/from CPU.
1386+
*
1387+
* Example kernel definition (CXX API):
1388+
* Ort::KernelDef kernel_def = Ort::KernelDefBuilder()
1389+
* .SetDomain("").SetOperatorType("Scan").SetSinceVersion(21, 22)
1390+
* .SetExecutionProvider("MyEp")
1391+
* .SetInputMemType(0, OrtMemTypeDefault) // input[0] on EP device
1392+
* .SetOutputMemType(0, OrtMemTypeDefault) // output[0] on EP device
1393+
* .AddTypeConstraint("V", ...).Build();
1394+
*
1395+
* \param[in] kernel_info The ::OrtKernelInfo instance for a Scan node. This function returns error ORT_FAIL
1396+
* if the opset version specified by `kernel_info` is unsupported.
1397+
* \param[in] helper A OrtScanKernelHelper instance that contains helper functions that ORT calls during kernel
1398+
* execution to operate on tensors allocated with the EP's device memory.
1399+
* ORT will call OrtScanKernelHelper::Release() to release the helper and its resources.
1400+
* \param[out] kernel_out Output parameter set to the OrtKernelImpl instance for the Scan node.
1401+
* Must be released via ::ReleaseKernelImpl, unless ownership is transferred
1402+
* to ORT (see OrtKernelCreateFunc and ::KernelRegistry_AddKernel()).
1403+
*
1404+
* \snippet{doc} snippets.dox OrtStatus Return Value
1405+
* \since Version 1.24
1406+
*/
1407+
ORT_API2_STATUS(CreateScanKernel, _In_ const OrtKernelInfo* kernel_info, _In_ OrtScanKernelHelper* helper,
1408+
_Outptr_ OrtKernelImpl** kernel_out);
1409+
1410+
ORT_CLASS_RELEASE(KernelImpl);
12201411
};
12211412

12221413
/**

onnxruntime/core/framework/session_state.cc

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,11 +1183,15 @@ const InlinedHashSet<NodeIndex>* SessionState::GetToBeExecutedRange(
11831183
Status SessionState::CreateSubgraphSessionState() {
11841184
for (auto& node : graph_.Nodes()) {
11851185
for (auto& entry : node.GetAttributeNameToMutableSubgraphMap()) {
1186-
const auto& ep = node.GetExecutionProviderType();
1187-
if (!ep.empty() &&
1188-
ep != kCpuExecutionProvider && ep != kCudaExecutionProvider &&
1189-
ep != kDmlExecutionProvider &&
1190-
ep != kJsExecutionProvider && ep != kWebGpuExecutionProvider) {
1186+
const auto& ep_type = node.GetExecutionProviderType();
1187+
const IExecutionProvider* ep = execution_providers_.Get(ep_type);
1188+
const bool is_plugin_ep = ep != nullptr && ep->GetOrtEp() != nullptr;
1189+
1190+
if (!ep_type.empty() &&
1191+
ep_type != kCpuExecutionProvider && ep_type != kCudaExecutionProvider &&
1192+
ep_type != kDmlExecutionProvider &&
1193+
ep_type != kJsExecutionProvider && ep_type != kWebGpuExecutionProvider &&
1194+
!is_plugin_ep) {
11911195
// SessionState is only used when ORT is executing the subgraph. If a non-ORT EP has taken the control flow
11921196
// node containing the subgraph it will create whatever state it needs internally.
11931197
continue;

onnxruntime/core/providers/cpu/controlflow/loop.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ class Loop : public controlflow::IControlFlowKernel {
4848

4949
static std::unique_ptr<OpKernel> Create(const OpKernelInfo& info, const ConcatOutput& concat_output_func, void* stream);
5050

51-
protected:
5251
// derived class can provide implementation for handling concatenation of Loop output on a different device
5352
void SetConcatOutputFunc(const ConcatOutput& concat_output_func) { concat_output_func_ = concat_output_func; }
5453

0 commit comments

Comments
 (0)