Add API to get ep graph partitioning info (#26781)
### Description
- Adds API functions to get information about the subgraphs/nodes
assigned to the EPs in the session.
- `Session_GetEpGraphAssignmentInfo`: Returns a list of "subgraphs",
each with information about the assigned EP and nodes.
- Note: App must enable session configuration
`"session.record_ep_graph_assignment_info"` to signal ORT to collect
this information. If not enabled, API returns empty results.
- `EpAssignedSubgraph_GetEpName`: Returns the name of the EP to which
the subgraph is assigned
- `EpAssignedSubgraph_GetNodes`: Returns a list of assigned nodes
- `EpAssignedNode_GetName`: Returns the assigned node's name
- `EpAssignedNode_GetDomain`: Returns the assigned node's domain
- `EpAssignedNode_GetOperatorType`: Returns the assigned node's operator
type
- Also adds C++ and Python bindings
#### Structure of returned information
The API returns a list of "subgraphs". Each subgraph has the following
information:
- Subgraph info:
- EP name: The name of the execution provider to which this subgraph is
assigned.
- nodes: Name and operator type of each node. Ex: `[{"multiply", "Mul"},
...]`
Python example program (taken from unit tests):
```python
def test_get_graph_provider_assignment_info(self):
"""
Tests querying for information about the nodes assigned to the CPU EP.
"""
# Create session options that enables recording EP graph partitioning info.
session_options = onnxrt.SessionOptions()
session_options.add_session_config_entry("session.record_ep_graph_assignment_info", "1")
session = onnxrt.InferenceSession(get_name("add_mul_add.onnx"), sess_options=session_options)
# Query session for information on each subgraph assigned to an EP.
ep_subgraphs = session.get_provider_graph_assignment_info()
# Check that all 3 nodes are assigned to CPU EP (each in its own subgraph)
self.assertEqual(len(ep_subgraphs), 3)
for ep_subgraph in ep_subgraphs:
self.assertEqual(ep_subgraph.ep_name, "CPUExecutionProvider")
self.assertEqual(len(ep_subgraph.get_nodes()), 1)
# Serialize each node to an identifier (concatenates operator type and node name)
node_ids: list[str] = [f"{n.op_type}/{n.name}" for s in ep_subgraphs for n in s.get_nodes()]
# Should have 1 Mul and 2 Adds.
self.assertEqual(len(node_ids), 3)
self.assertIn("Add/add_0", node_ids)
self.assertIn("Add/add_1", node_ids)
self.assertIn("Mul/mul_0", node_ids)
```
C++ program (taken from unit test):
```c++
// Check the ep graph partitioning (Mul on plugin EP, others on CPU EP).
// Model has 3 subgraphs (in no particular order):
// - Subgraph 1: Add assigned to CPU EP.
// - Subgraph 2: Mul assigned to plugin EP.
// - Subgraph 3: Add assigned to CPU EP.
std::vector<Ort::ConstEpAssignedSubgraph> ep_subgraphs = session.GetEpGraphAssignmentInfo();
ASSERT_EQ(ep_subgraphs.size(), 3);
for (Ort::ConstEpAssignedSubgraph ep_subgraph : ep_subgraphs) {
std::string ep_name = ep_subgraph.EpName();
ASSERT_TRUE(ep_name == Utils::example_ep_info.ep_name || ep_name == kCpuExecutionProvider);
const std::vector<Ort::ConstEpAssignedNode> ep_nodes = ep_subgraph.GetNodes();
ASSERT_GE(ep_nodes.size(), 1); // All of these subgraphs just have one node.
if (ep_name == kCpuExecutionProvider) {
std::string op_type = ep_nodes[0].OpType();
std::string node_name = ep_nodes[0].Name();
ASSERT_EQ(op_type, "Add");
ASSERT_TRUE(node_name == "add_0" || node_name == "add_1");
} else {
ASSERT_TRUE(ep_name == Utils::example_ep_info.ep_name);
std::string op_type = ep_nodes[0].OpType();
std::string node_name = ep_nodes[0].Name();
ASSERT_EQ(op_type, "Mul");
ASSERT_EQ(node_name, "mul_0");
}
}
```
### 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. -->
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>