Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
661a530
init
chilo-ms Jun 26, 2025
2784498
update comments
chilo-ms Jun 27, 2025
e1eca15
address lintrunner issue
chilo-ms Jun 27, 2025
4db3002
update comment to better review
chilo-ms Jun 27, 2025
3370de7
clean up and fix a compile warning
chilo-ms Jun 27, 2025
3077677
update test
chilo-ms Jun 27, 2025
256d055
merge main
chilo-ms Jul 5, 2025
e039ac9
refactor the code and address reviewers' comments
chilo-ms Jul 5, 2025
010f51f
update API comment
chilo-ms Jul 5, 2025
2439718
address reviewer's comments
chilo-ms Jul 5, 2025
9232c85
fix to change the function name
chilo-ms Jul 5, 2025
f686ba8
add an option to construct the sub-graph as a standalone OrtGraph.
chilo-ms Jul 6, 2025
86d4779
address reviewer comments
chilo-ms Jul 7, 2025
0589766
comment out the debug code
chilo-ms Jul 7, 2025
6e4dbee
address lintrunner issue
chilo-ms Jul 7, 2025
5246851
Add ORT_UNUSED_PARAMETER to address the build issue in minimal build
chilo-ms Jul 7, 2025
211e305
address reviewer comment
chilo-ms Jul 7, 2025
d5ec60a
fix bug
chilo-ms Jul 7, 2025
ecbeffb
remove the option to create a standalone OrtGraph
chilo-ms Jul 8, 2025
004de71
update comment
chilo-ms Jul 8, 2025
46c5dca
Merge branch 'main' into chi/add_graph_getsubgraph
chilo-ms Jul 8, 2025
517cf02
Add another edge case test for nother 3-layer nested graph
chilo-ms Jul 9, 2025
f58b4d5
Merge branch 'main' into chi/add_graph_getsubgraph
chilo-ms Jul 9, 2025
c15f43d
remove file that accidentally uploaded
chilo-ms Jul 9, 2025
7896ea8
revert back that in unit test to use half of the nodes to create OrtG…
chilo-ms Jul 9, 2025
57f851e
address reviewer comment
chilo-ms Jul 9, 2025
2fa60e2
Merge branch 'main' into chi/add_graph_getsubgraph
chilo-ms Jul 10, 2025
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
20 changes: 20 additions & 0 deletions include/onnxruntime/core/session/onnxruntime_c_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -5706,6 +5706,26 @@ struct OrtApi {
*/
ORT_API2_STATUS(Graph_GetParentNode, _In_ const OrtGraph* graph, _Outptr_result_maybenull_ const OrtNode** node);

/** \brief Create a sub-graph from a subset of nodes in an OrtGraph.
*
* NOTE: A 'sub-graph' is a graph formed by a subset of nodes within the current OrtGraph.
Comment thread
chilo-ms marked this conversation as resolved.
Outdated
* However, the control flow nodes have nested Graph instance/s which are called 'subgraph'.
*
* Regarding how initializers should be handled when constructing a new graph, in some cases,
* initializers that refer to a memory location in OrtValue can not be handled by some hardware backends (unlike those that are on disk).
* This prevents us from sharing the data and we have to make a copy here. In that case, set copy_in_memory_initializer to true.
*
* \param[in] graph The source OrtGraph instance.
* \param[in] nodes A subset of the nodes/OrtNodes in 'graph'.
* \param[in] copy_in_memory_initializer When constructing the graph, do copy the initializers from source graph to dst graph.
* \param[out] sub_graph An OrtGraph created from a given set of nodes. Must be released by calling ReleaseGraph.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.23.
*/
ORT_API2_STATUS(Graph_GetSubGraph, _In_ const OrtGraph* graph, _In_ const OrtNode** nodes, _In_ size_t num_nodes, _In_ bool copy_in_memory_initializer, _Outptr_ OrtGraph** sub_graph);

/// @}

/// \name OrtNode
Expand Down
21 changes: 18 additions & 3 deletions onnxruntime/core/graph/ep_api_types.cc
Original file line number Diff line number Diff line change
Expand Up @@ -499,10 +499,10 @@ void EpGraph::IndexToEpNodeMap::SetEpNode(NodeIndex node_index, EpNode* ep_node)
EpGraph::EpGraph(const GraphViewer& graph_viewer, PrivateTag)
: OrtGraph(OrtGraphIrApi::kEpApi), graph_viewer_(graph_viewer) {}

// Static class function to create a std::unique_ptr<EpGraph>.
Status EpGraph::Create(const GraphViewer& graph_viewer, /*out*/ std::unique_ptr<EpGraph>& result) {
auto ep_graph = std::make_unique<EpGraph>(graph_viewer, PrivateTag{});
EpGraph::EpGraph(std::unique_ptr<GraphViewer> graph_viewer, std::unique_ptr<Model> model, PrivateTag)
: OrtGraph(OrtGraphIrApi::kEpApi), graph_viewer_(*graph_viewer.get()), model_(std::move(model)), graph_viewer_from_graph_in_model_(std::move(graph_viewer)) {}

Status EpGraph::GreateImpl(std::unique_ptr<EpGraph> ep_graph, const GraphViewer& graph_viewer, /*out*/ std::unique_ptr<EpGraph>& result) {
Comment thread
adrianlizarraga marked this conversation as resolved.
Outdated
AllocatorPtr initializer_allocator = CPUAllocator::DefaultInstance();
std::unordered_map<std::string, std::unique_ptr<EpValueInfo>> value_infos_map;

Expand Down Expand Up @@ -656,6 +656,21 @@ Status EpGraph::Create(const GraphViewer& graph_viewer, /*out*/ std::unique_ptr<
return Status::OK();
}

// Static class function to create a std::unique_ptr<EpGraph>.
Status EpGraph::Create(std::unique_ptr<GraphViewer> graph_viewer_in_model, std::unique_ptr<Model> model, /*out*/ std::unique_ptr<EpGraph>& result) {
auto& graph_viewer = *graph_viewer_in_model.get();
auto ep_graph = std::make_unique<EpGraph>(std::move(graph_viewer_in_model), std::move(model), PrivateTag{});

return GreateImpl(std::move(ep_graph), graph_viewer, result);
}

// Static class function to create a std::unique_ptr<EpGraph>.
Status EpGraph::Create(const GraphViewer& graph_viewer, /*out*/ std::unique_ptr<EpGraph>& result) {
auto ep_graph = std::make_unique<EpGraph>(graph_viewer, PrivateTag{});

return GreateImpl(std::move(ep_graph), graph_viewer, result);
}

const std::string& EpGraph::GetName() const { return graph_viewer_.Name(); }

int64_t EpGraph::GetOnnxIRVersion() const { return graph_viewer_.GetOnnxIRVersion(); }
Expand Down
27 changes: 27 additions & 0 deletions onnxruntime/core/graph/ep_api_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "core/graph/basic_types.h"
#include "core/graph/abi_graph_types.h"
#include "core/graph/graph_viewer.h"
#include "core/graph/model.h"

namespace onnxruntime {
struct EpGraph;
Expand Down Expand Up @@ -249,15 +250,38 @@ struct EpGraph : public OrtGraph {

public:
EpGraph(const GraphViewer& graph_viewer, PrivateTag);
EpGraph(std::unique_ptr<GraphViewer> graph_viewer, std::unique_ptr<Model> model, PrivateTag);

/// <summary>
/// Creates an instance of EpGraph, which wraps a GraphViewer.
/// This call is used when creating an EpGraph from a GraphViewer instance. The GraphViewer instance is not onwed by this EpGraph.
/// </summary>
/// <param name="graph_viewer"></param>
/// <param name="result"></param>
/// <returns></returns>
static Status Create(const GraphViewer& graph_viewer, /*out*/ std::unique_ptr<EpGraph>& result);

/// <summary>
/// Creates an instance of EpGraph, which wraps a GraphViewer.
/// This call is used when creating an EpGraph from a subset of nodes in another EpGraph.
/// In this case, due to the implementation of OrtApis::Graph_GetSubGraph, the new EpGraph instance must take ownership of both the GraphViewer and the associated Model.
/// </summary>
/// <param name="graph_viewer"></param>
/// <param name="model"></param>
/// <param name="result"></param>
/// <returns></returns>
static Status Create(std::unique_ptr<GraphViewer> graph_viewer, std::unique_ptr<Model> model, /*out*/ std::unique_ptr<EpGraph>& result);

/// <summary>
/// The real implementation of creating an EpGraph instance.
/// Please use one of the above 'Create' functions that internally call this function, and avoid calling this function directly.
/// </summary>
/// <param name="ep_graph"></param>
/// <param name="graph_viewer"></param>
/// <param name="result"></param>
/// <returns></returns>
static Status GreateImpl(std::unique_ptr<EpGraph> ep_graph, const GraphViewer& graph_viewer, /*out*/ std::unique_ptr<EpGraph>& result);
Comment thread
adrianlizarraga marked this conversation as resolved.
Outdated

// Defines ToExternal() and ToInternal() functions to convert between OrtGraph and EpGraph.
DEFINE_ORT_GRAPH_IR_TO_EXTERNAL_INTERNAL_FUNCS(OrtGraph, EpGraph, OrtGraphIrApi::kEpApi)

Expand Down Expand Up @@ -324,6 +348,9 @@ struct EpGraph : public OrtGraph {
const GraphViewer& graph_viewer_;
const EpNode* parent_node_ = nullptr;

std::unique_ptr<Model> model_ = nullptr;
std::unique_ptr<GraphViewer> graph_viewer_from_graph_in_model_ = nullptr;

std::vector<std::unique_ptr<EpNode>> nodes_;
IndexToEpNodeMap index_to_ep_node_;

Expand Down
91 changes: 91 additions & 0 deletions onnxruntime/core/session/onnxruntime_c_api.cc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
#include "core/graph/graph.h"
#include "core/graph/model_editor_api_types.h"
#include "core/graph/ep_api_types.h"
#include "core/graph/model.h"
#include "core/graph/graph_utils.h"
#include "core/providers/get_execution_providers.h"
#include "core/session/abi_session_options_impl.h"
#include "core/session/allocator_adapters.h"
Expand Down Expand Up @@ -2691,6 +2693,94 @@
API_IMPL_END
}

ORT_API_STATUS_IMPL(OrtApis::Graph_GetSubGraph, _In_ const OrtGraph* src_graph,
_In_ const OrtNode** nodes,
_In_ size_t num_nodes,
_In_ bool copy_in_memory_initializer,
_Outptr_ OrtGraph** dst_graph) {
API_IMPL_BEGIN
const GraphViewer& graph_viewer = EpGraph::ToInternal(src_graph)->GetGraphViewer();
graph_viewer.DomainToVersionMap();

// This API constructs an onnxruntime::Graph from scratch using a given set of nodes,
Comment thread
adrianlizarraga marked this conversation as resolved.
Outdated
// obtains a corresponding onnxruntime::GraphViewer, and passes it to EpGraph::Create to create an EpGraph instance.

// The goal is to first construct an onnxruntime::Graph instance.
// The Graph constructor requires a pointer to an ONNX::GraphProto.
// Therefore it's simpler to create an onnxruntime::Model which holds both Graph and ONNX::ModelProto (contains ONNX::GraphProto)
std::unique_ptr<Model> model = std::make_unique<Model>(graph_viewer.Name(), true, graph_viewer.GetGraph().GetLogger());

Check failure on line 2711 in onnxruntime/core/session/onnxruntime_c_api.cc

View workflow job for this annotation

GitHub Actions / webgpu_minimal_build_edge_build_x64_RelWithDebInfo

'GetLogger': is not a member of 'onnxruntime::Graph'
Graph& new_graph = model->MainGraph();

// Builds the new graph by adding the node one by one
for (size_t node_idx = 0; node_idx < num_nodes; node_idx++) {
const OrtNode* ort_node = nodes[node_idx];

// TODO: might need to check the OrtNode is also in src_graph

Check warning on line 2718 in onnxruntime/core/session/onnxruntime_c_api.cc

View workflow job for this annotation

GitHub Actions / Optional Lint C++

[cpplint] reported by reviewdog 🐶 Missing username in TODO; it should look like "// TODO(my_username): Stuff." [readability/todo] [2] Raw Output: onnxruntime/core/session/onnxruntime_c_api.cc:2718: Missing username in TODO; it should look like "// TODO(my_username): Stuff." [readability/todo] [2]

const auto& ep_node = EpNode::ToInternal(ort_node);
if (ep_node == nullptr) {
return OrtApis::CreateStatus(OrtErrorCode::ORT_INVALID_ARGUMENT, "node should be a EpNode.");
}

const auto& node = ep_node->GetInternalNode();
std::vector<onnxruntime::NodeArg*> inputs, outputs;

for (auto input : node.InputDefs()) {
auto& node_arg = new_graph.GetOrCreateNodeArg(input->Name(), input->TypeAsProto());
inputs.push_back(&node_arg);
graph_utils::MakeInitializerCopyIfNotExist(graph_viewer.GetGraph(), new_graph, input->Name(),
copy_in_memory_initializer);
}

for (auto output : node.OutputDefs()) {
auto& node_arg = new_graph.GetOrCreateNodeArg(output->Name(), output->TypeAsProto());
outputs.push_back(&node_arg);
}

if (node.ContainsSubgraph()) {
for (auto input : node.ImplicitInputDefs()) {
graph_utils::MakeInitializerCopyIfNotExist(graph_viewer.GetGraph(), new_graph, input->Name(),
copy_in_memory_initializer);
}
}

// Updates node attributes if any.
// Ex: if the node has subgraph, it's possible that the subgraph and the GraphProto in node attribute are not in sync because of graph optimization.
// Therefore, we need to force GraphProto attribute to be updated in order to get the valid GraphProto.
if (node.GetAttributes().size() > 0) {
auto node_proto = std::make_unique<ONNX_NAMESPACE::NodeProto>();
// we need to update any GraphProto attributes for subgraphs so that any changes made by things
// such as the optimizers are captured. otherwise we can end up saving an invalid graph.
node.ToProto(*node_proto, /* update_subgraphs */ true);

Check failure on line 2754 in onnxruntime/core/session/onnxruntime_c_api.cc

View workflow job for this annotation

GitHub Actions / webgpu_minimal_build_edge_build_x64_RelWithDebInfo

'ToProto': is not a member of 'onnxruntime::Node'
const int num_attributes = node_proto->attribute_size();
auto node_attributes = std::make_unique<NodeAttributes>();
node_attributes->reserve(num_attributes);

for (int i = 0; i < num_attributes; ++i) {
auto& attr = node_proto->attribute(i);
node_attributes->emplace(attr.name(), attr);
}

// The GraphProto attributes are the updated ones.
new_graph.AddNode(node.Name(), node.OpType(), node.Description(), inputs, outputs, node_attributes.get(), node.Domain());
} else {
// The GraphProto attributes are the original ones.
new_graph.AddNode(node.Name(), node.OpType(), node.Description(), inputs, outputs, &node.GetAttributes(), node.Domain());
}
}

ORT_API_RETURN_IF_STATUS_NOT_OK(new_graph.Resolve());

Check failure on line 2772 in onnxruntime/core/session/onnxruntime_c_api.cc

View workflow job for this annotation

GitHub Actions / webgpu_minimal_build_edge_build_x64_RelWithDebInfo

'OrtStatus *onnxruntime::ToOrtStatus(const onnxruntime::common::Status &)': cannot convert argument 1 from 'int' to 'const onnxruntime::common::Status &'

Check failure on line 2772 in onnxruntime/core/session/onnxruntime_c_api.cc

View workflow job for this annotation

GitHub Actions / webgpu_minimal_build_edge_build_x64_RelWithDebInfo

'_status': cannot be used before it is initialized

Check failure on line 2772 in onnxruntime/core/session/onnxruntime_c_api.cc

View workflow job for this annotation

GitHub Actions / webgpu_minimal_build_edge_build_x64_RelWithDebInfo

'Resolve': is not a member of 'onnxruntime::Graph'

auto new_graph_viewer = std::make_unique<GraphViewer>(new_graph);
std::unique_ptr<EpGraph> result;
ORT_API_RETURN_IF_STATUS_NOT_OK(EpGraph::Create(std::move(new_graph_viewer), std::move(model), result));

*dst_graph = result.release();

return nullptr;
API_IMPL_END
}

//
// OrtNode
//
Expand Down Expand Up @@ -3603,6 +3693,7 @@
&OrtApis::Graph_GetNumNodes,
&OrtApis::Graph_GetNodes,
&OrtApis::Graph_GetParentNode,
&OrtApis::Graph_GetSubGraph,
&OrtApis::Node_GetId,
&OrtApis::Node_GetName,
&OrtApis::Node_GetOperatorType,
Expand Down
2 changes: 2 additions & 0 deletions onnxruntime/core/session/ort_apis.h
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,8 @@ ORT_API_STATUS_IMPL(Graph_GetNumNodes, _In_ const OrtGraph* graph, _Out_ size_t*
ORT_API_STATUS_IMPL(Graph_GetNodes, const OrtGraph* graph,
_Out_writes_(num_nodes) const OrtNode** nodes, _In_ size_t num_nodes);
ORT_API_STATUS_IMPL(Graph_GetParentNode, _In_ const OrtGraph* graph, _Outptr_result_maybenull_ const OrtNode** node);
ORT_API_STATUS_IMPL(Graph_GetSubGraph, _In_ const OrtGraph* graph, _In_ const OrtNode** nodes, _In_ size_t num_nodes,
_In_ bool copy_in_memory_initializer, _Outptr_ OrtGraph** subgraph);

// OrtNode
ORT_API_STATUS_IMPL(Node_GetId, _In_ const OrtNode* node, _Out_ size_t* node_id);
Expand Down
57 changes: 57 additions & 0 deletions onnxruntime/test/ep_graph/test_ep_graph.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
#include <gsl/gsl>
#include <memory>
#include <vector>
#include <fstream>

#include "core/common/common.h"
#include "core/framework/tensorprotoutils.h"
#include "core/framework/tensor_type_and_shape.h"
#include "core/framework/onnxruntime_typeinfo.h"
#include "core/session/onnxruntime_cxx_api.h"
#include "core/graph/ep_api_types.h"
#include "core/graph/graph_proto_serializer.h"

#include "test/ep_graph/test_ep_graph_utils.h"
#include "test/util/include/api_asserts.h"
Expand All @@ -26,6 +29,7 @@ namespace test {
// forward-declaration for utility that uses public C APIs to check that an OrtGraph is equivalent
// to a graph represented by the internal ORT GraphViewer class.
static void CheckGraphCApi(const GraphViewer& graph_viewer, const OrtGraph& api_graph);
static void Check_Graph_GetSubgraph(const OrtGraph& api_graph);

//
// Tests
Expand Down Expand Up @@ -68,6 +72,15 @@ TEST(EpGraphTest, Check3LayerNestedSubgraph) {
CheckGraphCApi(test_graph->GetGraphViewer(), test_graph->GetOrtGraph());
}

/*
TEST(EpGraphTest, CApiUseOfGetSubGraphFromGraph) {
auto test_graph = TestGraph::Load(ORT_TSTR("testdata/mnist.onnx"));
ASSERT_NE(test_graph, nullptr) << "Failed to load test model";

Check_Graph_GetSubgraph(test_graph->GetOrtGraph());
}
*/

//
// Utils for traversing an OrtGraph and checking against GraphViewer.
//
Expand Down Expand Up @@ -307,6 +320,47 @@ static void CheckValueInfosCApi(const GraphViewer& graph_viewer, gsl::span<const
}
}

// Checks the Graph_GetSubgraph C API
static void Check_Graph_GetSubgraph(const OrtGraph& api_graph) {
const OrtApi& ort_api = Ort::GetApi();

// Get all the nodes
size_t num_nodes = 0;
ASSERT_ORTSTATUS_OK(ort_api.Graph_GetNumNodes(&api_graph, &num_nodes));

std::vector<const OrtNode*> nodes(num_nodes);
ASSERT_ORTSTATUS_OK(ort_api.Graph_GetNodes(&api_graph, nodes.data(), nodes.size()));

// Select a half of nodes to create a sub-graph
size_t num_selected_nodes = (nodes.size() >> 1);
std::vector<const OrtNode*> selected_nodes(num_selected_nodes);

for (size_t i = 0; i < num_selected_nodes ; i++) {
selected_nodes.push_back(nodes[i]);
Comment thread
chilo-ms marked this conversation as resolved.
Outdated
}

OrtGraph* sub_graph;
ASSERT_ORTSTATUS_OK(ort_api.Graph_GetSubGraph(&api_graph, selected_nodes.data(), selected_nodes.size(), true, &sub_graph));

// Convert OrtGraph to ModelProto and dump it to disk for debug purpose.
/*
const GraphViewer& sub_graph_viewer = EpGraph::ToInternal(sub_graph)->GetGraphViewer();
std::unique_ptr<Model> model = std::make_unique<Model>(sub_graph_viewer.Name(), true, sub_graph_viewer.GetGraph().GetLogger());
auto model_proto = std::make_unique<ONNX_NAMESPACE::ModelProto>(model->ToProto());
GraphViewerToProto(sub_graph_viewer, *model_proto->mutable_graph(), true, true, static_cast<ExecutionOrder>(1));
model_proto->set_ir_version(ONNX_NAMESPACE::Version::IR_VERSION);

std::string string_buf;
model_proto->SerializeToString(&string_buf);

// Dump TensorRT subgraph for debugging
std::fstream dump("Subgraph.onnx", std::ios::out | std::ios::trunc | std::ios::binary);
model_proto->SerializeToOstream(&dump);
*/

ort_api.ReleaseGraph(sub_graph);
}

// Checks that the contents of the original GraphViewer matches the contents of the OrtGraph.
// Uses the public C APIs to traverse the OrtGraph.
static void CheckGraphCApi(const GraphViewer& graph_viewer, const OrtGraph& api_graph) {
Expand Down Expand Up @@ -501,6 +555,9 @@ static void CheckGraphCApi(const GraphViewer& graph_viewer, const OrtGraph& api_
}
}
}

// Check creating an OrtGraph from a subset of nodes in an OrtGraph
Check_Graph_GetSubgraph(api_graph);
}

} // namespace test
Expand Down
Loading