Skip to content

Commit

Permalink
[SYCL][Graph] Enable empty nodes in Subgraphs (#300)
Browse files Browse the repository at this point in the history
* [SYCL][Graph] Enable empty nodes in Subgraphs

The implementation uses the list of scheduled nodes to add a subgraph to a main graph.
However, since empty nodes are not scheduled, empty nodes were not listed in this list, resulting in inconsistent graphs when subgraph with empty node(s) were added to a main graph.
This PR fixes this issue by forcing to list empty nodes when creating the list for inserting subgraph.
It also adds unitests to check that subgraphs with empty nodes are correctly added to a main graph.

* [SYCL][Graph] Enable empty nodes in Subgraphs

Changes the definition of MSchedule as it now contains all types of nodes (including empty nodes).
Empty nodes are now filtered out when creating the commandbuffer and/or enqueuing nodes to only keep their dependencies.
This PR updates a few unitests to make them compliant to the new schedule list definition.

* Update sycl/source/detail/graph_impl.cpp

Co-authored-by: Ben Tracy <ben.tracy@codeplay.com>

---------

Co-authored-by: Ben Tracy <ben.tracy@codeplay.com>
  • Loading branch information
mfrancepillois and Bensuo authored Aug 16, 2023
1 parent 80cf699 commit 5318388
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 17 deletions.
25 changes: 17 additions & 8 deletions sycl/source/detail/graph_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -176,28 +176,31 @@ std::shared_ptr<node_impl> graph_impl::addNodesToExits(
std::shared_ptr<node_impl> graph_impl::addSubgraphNodes(
const std::shared_ptr<exec_graph_impl> &SubGraphExec) {
std::map<std::shared_ptr<node_impl>, std::shared_ptr<node_impl>> NodesMap;
std::list<std::shared_ptr<node_impl>> NewNodeList;
std::list<std::shared_ptr<node_impl>> NewNodesList;

std::list<std::shared_ptr<node_impl>> NodeList = SubGraphExec->getSchedule();
std::list<std::shared_ptr<node_impl>> NodesList = SubGraphExec->getSchedule();

// Duplication of nodes
for (std::list<std::shared_ptr<node_impl>>::const_iterator NodeIt =
NodeList.end();
NodeIt != NodeList.begin();) {
NodesList.end();
NodeIt != NodesList.begin();) {
--NodeIt;
auto Node = *NodeIt;
std::shared_ptr<node_impl> NodeCopy;
duplicateNode(Node, NodeCopy);
NewNodeList.push_back(NodeCopy);
NewNodesList.push_back(NodeCopy);
NodesMap.insert({Node, NodeCopy});
for (auto &NextNode : Node->MSuccessors) {
if (NodesMap.find(NextNode) != NodesMap.end()) {
auto Successor = NodesMap[NextNode];
NodeCopy->registerSuccessor(Successor, NodeCopy);
} else {
assert("Node duplication failed. A duplicated node is missing.");
}
}
}

return addNodesToExits(NewNodeList);
return addNodesToExits(NewNodesList);
}

void graph_impl::addRoot(const std::shared_ptr<node_impl> &Root) {
Expand Down Expand Up @@ -523,6 +526,11 @@ void exec_graph_impl::createCommandBuffers(sycl::device Device) {

// TODO extract kernel bundle logic from enqueueImpKernel
for (auto Node : MSchedule) {
// Empty nodes are not processed as other nodes, but only their
// dependencies are propagated in findRealDeps
if (Node->isEmpty())
continue;

sycl::detail::CG::CGTYPE type = Node->MCGType;
// If the node is a kernel with no special requirements we can enqueue it
// directly.
Expand Down Expand Up @@ -663,8 +671,9 @@ exec_graph_impl::enqueue(const std::shared_ptr<sycl::detail::queue_impl> &Queue,
"Error during emulated graph command group submission.");
}
ScheduledEvents.push_back(NewEvent);
} else {

} else if (!NodeImpl->isEmpty()) {
// Empty nodes are node processed as other nodes, but only their
// dependencies are propagated in findRealDeps
sycl::detail::EventImplPtr EventImpl =
sycl::detail::Scheduler::getInstance().addCG(NodeImpl->getCGCopy(),
Queue);
Expand Down
6 changes: 2 additions & 4 deletions sycl/source/detail/graph_impl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,8 @@ class node_impl {
if (std::find(Schedule.begin(), Schedule.end(), Next) == Schedule.end())
Next->sortTopological(Next, Schedule);
}
// We don't need to schedule empty nodes as they are only used when
// calculating dependencies
if (!NodeImpl->isEmpty())
Schedule.push_front(NodeImpl);

Schedule.push_front(NodeImpl);
}

/// Checks if this node has a given requirement.
Expand Down
172 changes: 167 additions & 5 deletions sycl/unittests/Extensions/CommandGraph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -838,13 +838,160 @@ TEST_F(CommandGraphTest, SubGraph) {
auto MainGraphExecImpl = sycl::detail::getSyclObjImpl(MainGraphExec);
auto Schedule = MainGraphExecImpl->getSchedule();
auto ScheduleIt = Schedule.begin();
ASSERT_EQ(Schedule.size(), 4ul);
// The schedule list must contain 5 nodes: 4 regulars + 1 empty.
// Indeed an empty node is added as an exit point of the added subgraph to
// facilitate the handling of dependencies
ASSERT_EQ(Schedule.size(), 5ul);
ASSERT_EQ(*ScheduleIt, sycl::detail::getSyclObjImpl(Node1MainGraph));
ScheduleIt++;
ASSERT_TRUE(*(*ScheduleIt) == *(sycl::detail::getSyclObjImpl(Node1Graph)));
ScheduleIt++;
ASSERT_TRUE(*(*ScheduleIt) == *(sycl::detail::getSyclObjImpl(Node2Graph)));
ScheduleIt++;
ASSERT_TRUE((*ScheduleIt)->isEmpty());
ScheduleIt++;
ASSERT_EQ(*ScheduleIt, sycl::detail::getSyclObjImpl(Node3MainGraph));
ASSERT_EQ(Queue.get_context(), MainGraphExecImpl->getContext());
}

TEST_F(CommandGraphTest, SubGraphWithEmptyNode) {
// Add sub-graph with two nodes
auto Node1Graph = Graph.add(
[&](sycl::handler &cgh) { cgh.single_task<TestKernel<>>([]() {}); });
auto Empty1Graph =
Graph.add([&](sycl::handler &cgh) { /*empty node */ },
{experimental::property::node::depends_on(Node1Graph)});
auto Node2Graph = Graph.add(
[&](sycl::handler &cgh) { cgh.single_task<TestKernel<>>([]() {}); },
{experimental::property::node::depends_on(Empty1Graph)});

auto GraphExec = Graph.finalize();

// Add node to main graph followed by sub-graph and another node
experimental::command_graph MainGraph(Queue.get_context(), Dev);
auto Node1MainGraph = MainGraph.add(
[&](sycl::handler &cgh) { cgh.single_task<TestKernel<>>([]() {}); });
auto Node2MainGraph =
MainGraph.add([&](handler &CGH) { CGH.ext_oneapi_graph(GraphExec); },
{experimental::property::node::depends_on(Node1MainGraph)});
auto Node3MainGraph = MainGraph.add(
[&](sycl::handler &cgh) { cgh.single_task<TestKernel<>>([]() {}); },
{experimental::property::node::depends_on(Node2MainGraph)});

// Assert order of the added sub-graph
ASSERT_NE(sycl::detail::getSyclObjImpl(Node2MainGraph), nullptr);
ASSERT_TRUE(sycl::detail::getSyclObjImpl(Node2MainGraph)->isEmpty());
// Check the structure of the main graph.
// 1 root connected to 1 successor (the single root of the subgraph)
ASSERT_EQ(sycl::detail::getSyclObjImpl(MainGraph)->MRoots.size(), 1lu);
ASSERT_EQ(sycl::detail::getSyclObjImpl(Node1MainGraph)->MSuccessors.size(),
1lu);
// Subgraph nodes are duplicated when inserted to parent graph.
// we thus check the node content only.
ASSERT_TRUE(
*(sycl::detail::getSyclObjImpl(Node1MainGraph)->MSuccessors.front()) ==
*(sycl::detail::getSyclObjImpl(Node1Graph)));
ASSERT_EQ(sycl::detail::getSyclObjImpl(Node1MainGraph)->MSuccessors.size(),
1lu);
ASSERT_EQ(sycl::detail::getSyclObjImpl(Node2MainGraph)->MSuccessors.size(),
1lu);
ASSERT_EQ(sycl::detail::getSyclObjImpl(Node1MainGraph)->MPredecessors.size(),
0lu);
ASSERT_EQ(sycl::detail::getSyclObjImpl(Node2MainGraph)->MPredecessors.size(),
1lu);

// Finalize main graph and check schedule
auto MainGraphExec = MainGraph.finalize();
auto MainGraphExecImpl = sycl::detail::getSyclObjImpl(MainGraphExec);
auto Schedule = MainGraphExecImpl->getSchedule();
auto ScheduleIt = Schedule.begin();
// The schedule list must contain 6 nodes: 5 regulars + 1 empty.
// Indeed an empty node is added as an exit point of the added subgraph to
// facilitate the handling of dependencies
ASSERT_EQ(Schedule.size(), 6ul);
ASSERT_EQ(*ScheduleIt, sycl::detail::getSyclObjImpl(Node1MainGraph));
ScheduleIt++;
ASSERT_TRUE(*(*ScheduleIt) == *(sycl::detail::getSyclObjImpl(Node1Graph)));
ScheduleIt++;
ASSERT_TRUE((*ScheduleIt)->isEmpty()); // empty node inside the subgraph
ScheduleIt++;
ASSERT_TRUE(*(*ScheduleIt) == *(sycl::detail::getSyclObjImpl(Node2Graph)));
ScheduleIt++;
ASSERT_TRUE(
(*ScheduleIt)->isEmpty()); // empty node added by the impl to handle
// depenendcies w.r.t. the added subgraph
ScheduleIt++;
ASSERT_EQ(*ScheduleIt, sycl::detail::getSyclObjImpl(Node3MainGraph));
ASSERT_EQ(Queue.get_context(), MainGraphExecImpl->getContext());
}

TEST_F(CommandGraphTest, SubGraphWithEmptyNodeLast) {
// Add sub-graph with two nodes
auto Node1Graph = Graph.add(
[&](sycl::handler &cgh) { cgh.single_task<TestKernel<>>([]() {}); });
auto Node2Graph = Graph.add(
[&](sycl::handler &cgh) { cgh.single_task<TestKernel<>>([]() {}); },
{experimental::property::node::depends_on(Node1Graph)});
auto EmptyGraph =
Graph.add([&](sycl::handler &cgh) { /*empty node */ },
{experimental::property::node::depends_on(Node2Graph)});

auto GraphExec = Graph.finalize();

// Add node to main graph followed by sub-graph and another node
experimental::command_graph MainGraph(Queue.get_context(), Dev);
auto Node1MainGraph = MainGraph.add(
[&](sycl::handler &cgh) { cgh.single_task<TestKernel<>>([]() {}); });
auto Node2MainGraph =
MainGraph.add([&](handler &CGH) { CGH.ext_oneapi_graph(GraphExec); },
{experimental::property::node::depends_on(Node1MainGraph)});
auto Node3MainGraph = MainGraph.add(
[&](sycl::handler &cgh) { cgh.single_task<TestKernel<>>([]() {}); },
{experimental::property::node::depends_on(Node2MainGraph)});

// Assert order of the added sub-graph
ASSERT_NE(sycl::detail::getSyclObjImpl(Node2MainGraph), nullptr);
ASSERT_TRUE(sycl::detail::getSyclObjImpl(Node2MainGraph)->isEmpty());
// Check the structure of the main graph.
// 1 root connected to 1 successor (the single root of the subgraph)
ASSERT_EQ(sycl::detail::getSyclObjImpl(MainGraph)->MRoots.size(), 1lu);
ASSERT_EQ(sycl::detail::getSyclObjImpl(Node1MainGraph)->MSuccessors.size(),
1lu);
// Subgraph nodes are duplicated when inserted to parent graph.
// we thus check the node content only.
ASSERT_TRUE(
*(sycl::detail::getSyclObjImpl(Node1MainGraph)->MSuccessors.front()) ==
*(sycl::detail::getSyclObjImpl(Node1Graph)));
ASSERT_EQ(sycl::detail::getSyclObjImpl(Node1MainGraph)->MSuccessors.size(),
1lu);
ASSERT_EQ(sycl::detail::getSyclObjImpl(Node2MainGraph)->MSuccessors.size(),
1lu);
ASSERT_EQ(sycl::detail::getSyclObjImpl(Node1MainGraph)->MPredecessors.size(),
0lu);
ASSERT_EQ(sycl::detail::getSyclObjImpl(Node2MainGraph)->MPredecessors.size(),
1lu);

// Finalize main graph and check schedule
auto MainGraphExec = MainGraph.finalize();
auto MainGraphExecImpl = sycl::detail::getSyclObjImpl(MainGraphExec);
auto Schedule = MainGraphExecImpl->getSchedule();
auto ScheduleIt = Schedule.begin();
// The schedule list must contain 6 nodes: 5 regulars + 1 empty.
// Indeed an empty node is added as an exit point of the added subgraph to
// facilitate the handling of dependencies
ASSERT_EQ(Schedule.size(), 6ul);
ASSERT_EQ(*ScheduleIt, sycl::detail::getSyclObjImpl(Node1MainGraph));
ScheduleIt++;
ASSERT_TRUE(*(*ScheduleIt) == *(sycl::detail::getSyclObjImpl(Node1Graph)));
ScheduleIt++;
ASSERT_TRUE(*(*ScheduleIt) == *(sycl::detail::getSyclObjImpl(Node2Graph)));
ScheduleIt++;
ASSERT_TRUE((*ScheduleIt)->isEmpty()); // empty node inside the subgraph
ScheduleIt++;
ASSERT_TRUE(
(*ScheduleIt)->isEmpty()); // empty node added by the impl to handle
// depenendcies w.r.t. the added subgraph
ScheduleIt++;
ASSERT_EQ(*ScheduleIt, sycl::detail::getSyclObjImpl(Node3MainGraph));
ASSERT_EQ(Queue.get_context(), MainGraphExecImpl->getContext());
}
Expand Down Expand Up @@ -881,7 +1028,10 @@ TEST_F(CommandGraphTest, RecordSubGraph) {
auto MainGraphExecImpl = sycl::detail::getSyclObjImpl(MainGraphExec);
auto Schedule = MainGraphExecImpl->getSchedule();
auto ScheduleIt = Schedule.begin();
ASSERT_EQ(Schedule.size(), 4ul);
// The schedule list must contain 5 nodes: 4 regulars + 1 empty.
// Indeed an empty node is added as an exit point of the added subgraph to
// facilitate the handling of dependencies
ASSERT_EQ(Schedule.size(), 5ul);

// The first and fourth nodes should have events associated with MainGraph but
// not graph. The second and third nodes were added as a sub-graph and
Expand All @@ -904,6 +1054,9 @@ TEST_F(CommandGraphTest, RecordSubGraph) {
ASSERT_ANY_THROW(
sycl::detail::getSyclObjImpl(Graph)->getEventForNode(*ScheduleIt));

ScheduleIt++;
ASSERT_TRUE((*ScheduleIt)->isEmpty());

ScheduleIt++;
ASSERT_ANY_THROW(
sycl::detail::getSyclObjImpl(Graph)->getEventForNode(*ScheduleIt));
Expand Down Expand Up @@ -1023,9 +1176,12 @@ TEST_F(CommandGraphTest, InOrderQueueWithEmpty) {
auto GraphExecImpl = sycl::detail::getSyclObjImpl(GraphExec);
auto Schedule = GraphExecImpl->getSchedule();
auto ScheduleIt = Schedule.begin();
ASSERT_EQ(Schedule.size(), 2ul);
// the schedule list contains all types of nodes (even empty nodes)
ASSERT_EQ(Schedule.size(), 3ul);
ASSERT_EQ(*ScheduleIt, PtrNode1);
ScheduleIt++;
ASSERT_TRUE((*ScheduleIt)->isEmpty());
ScheduleIt++;
ASSERT_EQ(*ScheduleIt, PtrNode3);
ASSERT_EQ(InOrderQueue.get_context(), GraphExecImpl->getContext());
}
Expand Down Expand Up @@ -1080,7 +1236,10 @@ TEST_F(CommandGraphTest, InOrderQueueWithEmptyFirst) {
auto GraphExecImpl = sycl::detail::getSyclObjImpl(GraphExec);
auto Schedule = GraphExecImpl->getSchedule();
auto ScheduleIt = Schedule.begin();
ASSERT_EQ(Schedule.size(), 2ul);
// the schedule list contains all types of nodes (even empty nodes)
ASSERT_EQ(Schedule.size(), 3ul);
ASSERT_TRUE((*ScheduleIt)->isEmpty());
ScheduleIt++;
ASSERT_EQ(*ScheduleIt, PtrNode2);
ScheduleIt++;
ASSERT_EQ(*ScheduleIt, PtrNode3);
Expand Down Expand Up @@ -1137,10 +1296,13 @@ TEST_F(CommandGraphTest, InOrderQueueWithEmptyLast) {
auto GraphExecImpl = sycl::detail::getSyclObjImpl(GraphExec);
auto Schedule = GraphExecImpl->getSchedule();
auto ScheduleIt = Schedule.begin();
ASSERT_EQ(Schedule.size(), 2ul);
// the schedule list contains all types of nodes (even empty nodes)
ASSERT_EQ(Schedule.size(), 3ul);
ASSERT_EQ(*ScheduleIt, PtrNode1);
ScheduleIt++;
ASSERT_EQ(*ScheduleIt, PtrNode2);
ScheduleIt++;
ASSERT_TRUE((*ScheduleIt)->isEmpty());
ASSERT_EQ(InOrderQueue.get_context(), GraphExecImpl->getContext());
}

Expand Down

0 comments on commit 5318388

Please sign in to comment.