diff --git a/level_zero/api/driver_experimental/public/zex_graph.cpp b/level_zero/api/driver_experimental/public/zex_graph.cpp index b1bb0e534e..0246fb85e2 100644 --- a/level_zero/api/driver_experimental/public/zex_graph.cpp +++ b/level_zero/api/driver_experimental/public/zex_graph.cpp @@ -196,7 +196,21 @@ ze_result_t ZE_APICALL zeGraphIsEmptyExp(ze_graph_handle_t hGraph) { } ze_result_t ZE_APICALL zeGraphDumpContentsExp(ze_graph_handle_t hGraph, const char *filePath, void *pNext) { - return ZE_RESULT_ERROR_UNSUPPORTED_FEATURE; + if (nullptr != pNext) { + return ZE_RESULT_ERROR_INVALID_ARGUMENT; + } + + auto graph = L0::Graph::fromHandle(hGraph); + if (nullptr == graph) { + return ZE_RESULT_ERROR_INVALID_ARGUMENT; + } + + if (nullptr == filePath) { + return ZE_RESULT_ERROR_INVALID_ARGUMENT; + } + + L0::GraphDotExporter exporter{}; + return exporter.exportToFile(*graph, filePath); } } // namespace L0 diff --git a/level_zero/core/test/black_box_tests/zello_graph.cpp b/level_zero/core/test/black_box_tests/zello_graph.cpp index 7683d8c53a..20f86483e4 100644 --- a/level_zero/core/test/black_box_tests/zello_graph.cpp +++ b/level_zero/core/test/black_box_tests/zello_graph.cpp @@ -11,9 +11,9 @@ #include "zello_compile.h" #include -#include #include -#include + +#define ENABLE_GRAPH_DUMP false using zeGraphCreateExpFP = ze_result_t(ZE_APICALL *)(ze_context_handle_t context, ze_graph_handle_t *phGraph, void *pNext); using zeCommandListBeginGraphCaptureExpFP = ze_result_t(ZE_APICALL *)(ze_command_list_handle_t hCommandList, void *pNext); @@ -48,6 +48,21 @@ struct GraphApi { } }; +void dumpGraphToDotIfEnabled(const GraphApi &graphApi, ze_graph_handle_t virtualGraph, const std::string &testName) { + if (!ENABLE_GRAPH_DUMP) { + return; + } + + std::string filename = testName + "_graph.gv"; + ze_result_t dumpResult = graphApi.graphDumpContents(virtualGraph, filename.c_str(), nullptr); + + if (dumpResult == ZE_RESULT_SUCCESS) { + std::cout << "Graph dumped to " << filename << std::endl; + } else { + std::cerr << "Failed to dump graph for test " << testName << " (result: " << std::hex << dumpResult << ")" << std::endl; + } +} + GraphApi loadGraphApi(ze_driver_handle_t driver) { GraphApi ret; zeDriverGetExtensionFunctionAddress(driver, "zeGraphCreateExp", reinterpret_cast(&ret.graphCreate)); @@ -130,6 +145,8 @@ void testAppendMemoryCopy(ze_driver_handle_t driver, ze_context_handle_t &contex std::cerr << "stackBuffer == " << static_cast(stackBuffer) << std::endl; } + dumpGraphToDotIfEnabled(graphApi, virtualGraph, __func__); + delete[] heapBuffer; SUCCESS_OR_TERMINATE(zeMemFree(context, zeBuffer)); @@ -225,6 +242,8 @@ void testMultiGraph(ze_driver_handle_t driver, ze_context_handle_t &context, ze_ std::cerr << "stackBuffer == " << static_cast(stackBuffer) << std::endl; } + dumpGraphToDotIfEnabled(graphApi, virtualGraph, __func__); + delete[] heapBuffer; SUCCESS_OR_TERMINATE(zeMemFree(context, zeBuffer)); @@ -444,6 +463,8 @@ void testAppendLaunchKernel(ze_driver_handle_t driver, std::cerr << "outputData == " << static_cast(outputData.get()) << std::endl; } + dumpGraphToDotIfEnabled(graphApi, virtualGraph, __func__); + // Cleanup SUCCESS_OR_TERMINATE(zeMemFree(context, dstBuffer)); SUCCESS_OR_TERMINATE(zeMemFree(context, interimBuffer)); @@ -608,6 +629,8 @@ void testAppendLaunchMultipleKernelsIndirect(ze_driver_handle_t driver, std::cerr << "expectedData == " << static_cast(expectedIncremented.data()) << std::endl; } + dumpGraphToDotIfEnabled(graphApi, virtualGraph, __func__); + // Cleanup SUCCESS_OR_TERMINATE(zeMemFree(context, dispatchTraits)); SUCCESS_OR_TERMINATE(zeMemFree(context, kernelsNumBuff)); diff --git a/level_zero/core/test/unit_tests/experimental/CMakeLists.txt b/level_zero/core/test/unit_tests/experimental/CMakeLists.txt index 7d8f5d67ed..089ffa12dd 100644 --- a/level_zero/core/test/unit_tests/experimental/CMakeLists.txt +++ b/level_zero/core/test/unit_tests/experimental/CMakeLists.txt @@ -7,6 +7,7 @@ target_sources(${TARGET_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.txt ${CMAKE_CURRENT_SOURCE_DIR}/test_graph.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_graph_exporter.cpp ) target_include_directories(${TARGET_NAME} diff --git a/level_zero/core/test/unit_tests/experimental/test_graph.cpp b/level_zero/core/test/unit_tests/experimental/test_graph.cpp index f4012f1eea..f06ccdaa2a 100644 --- a/level_zero/core/test/unit_tests/experimental/test_graph.cpp +++ b/level_zero/core/test/unit_tests/experimental/test_graph.cpp @@ -328,14 +328,6 @@ TEST(GraphTestDebugApis, GivenNonEmptyGraphWhenGraphIsEmptyIsCalledThenErrorIsRe EXPECT_EQ(ZE_RESULT_QUERY_FALSE, ::zeGraphIsEmptyExp(&srcGraph)); } -TEST(GraphTestDebugApis, WhenGraphDumpContentsIsCalledThenReturnUnsupportedFeature) { - GraphsCleanupGuard graphCleanup; - Mock ctx; - L0::Graph srcGraph(&ctx, true); - auto err = ::zeGraphDumpContentsExp(&srcGraph, "dump", nullptr); - EXPECT_EQ(ZE_RESULT_ERROR_UNSUPPORTED_FEATURE, err); -} - TEST(GraphTestApiSubmit, GivenNonNullPNextThenGraphAppendReturnsError) { GraphsCleanupGuard graphCleanup; Mock ctx; diff --git a/level_zero/core/test/unit_tests/experimental/test_graph_exporter.cpp b/level_zero/core/test/unit_tests/experimental/test_graph_exporter.cpp new file mode 100644 index 0000000000..6a98dbdfae --- /dev/null +++ b/level_zero/core/test/unit_tests/experimental/test_graph_exporter.cpp @@ -0,0 +1,535 @@ +/* + * Copyright (C) 2025 Intel Corporation + * + * SPDX-License-Identifier: MIT + * + */ + +#include "shared/test/common/helpers/variable_backup.h" +#include "shared/test/common/mocks/mock_io_functions.h" + +#include "level_zero/core/test/unit_tests/experimental/test_graph.h" + +#include "gtest/gtest.h" + +using namespace NEO; + +namespace L0 { +namespace ult { + +class MockGraphDotExporter : public GraphDotExporter { + public: + using GraphDotExporter::exportToString; + using GraphDotExporter::findSubgraphIndex; + using GraphDotExporter::findSubgraphIndexByCommandList; + using GraphDotExporter::generateNodeId; + using GraphDotExporter::generateSubgraphId; + using GraphDotExporter::getCommandNodeAttributes; + using GraphDotExporter::getCommandNodeLabel; + using GraphDotExporter::getSubgraphFillColor; + using GraphDotExporter::writeEdges; + using GraphDotExporter::writeForkJoinEdges; + using GraphDotExporter::writeHeader; + using GraphDotExporter::writeNodes; + using GraphDotExporter::writeSubgraphs; + using GraphDotExporter::writeUnjoinedForkEdges; +}; + +class GraphDotExporterTest : public ::testing::Test { + protected: + GraphsCleanupGuard graphCleanup; + Mock ctx; + MockGraphDotExporter exporter; + const std::string testFilePath = "test_graph_export.gv"; +}; + +TEST_F(GraphDotExporterTest, GivenNullFilePathWhenExportToFileThenReturnsInvalidArgument) { + Graph testGraph{&ctx, true}; + auto result = exporter.exportToFile(testGraph, nullptr); + EXPECT_EQ(ZE_RESULT_ERROR_INVALID_ARGUMENT, result); +} + +TEST_F(GraphDotExporterTest, GivenEmptyGraphWhenExportToStringThenContainsDigraphHeader) { + Graph testGraph{&ctx, true}; + + std::string dot = exporter.exportToString(testGraph); + EXPECT_NE(dot.find("digraph \"graph\" {"), std::string::npos); + EXPECT_NE(dot.find("rankdir=TB;"), std::string::npos); + EXPECT_NE(dot.find("nodesep=1;"), std::string::npos); + EXPECT_NE(dot.find("ranksep=1;"), std::string::npos); + EXPECT_NE(dot.find("node [shape=box, style=filled];"), std::string::npos); + EXPECT_NE(dot.find("edge [color=black];"), std::string::npos); + EXPECT_NE(dot.find('}'), std::string::npos); +} + +TEST_F(GraphDotExporterTest, GivenGraphWithSingleCommandWhenExportToStringThenContainsCommandNode) { + Graph testGraph{&ctx, true}; + Mock event; + Mock cmdlist; + + testGraph.capture(&cmdlist, &event, 0U, nullptr); + testGraph.stopCapturing(); + + std::string dot = exporter.exportToString(testGraph); + EXPECT_NE(dot.find("zeCommandListAppendBarrier"), std::string::npos); + EXPECT_NE(dot.find("L0_S0_C0"), std::string::npos); +} + +TEST_F(GraphDotExporterTest, GivenGraphWithMultipleCommandsWhenExportToStringThenContainsSequentialEdges) { + Graph testGraph{&ctx, true}; + Mock event; + Mock cmdlist; + + testGraph.capture(&cmdlist, &event, 0U, nullptr); + testGraph.capture(&cmdlist, nullptr, nullptr, 0U, nullptr, 0U, nullptr); + testGraph.stopCapturing(); + + std::string dot = exporter.exportToString(testGraph); + EXPECT_NE(dot.find("L0_S0_C0 -> L0_S0_C1"), std::string::npos); +} + +TEST_F(GraphDotExporterTest, WhenWriteHeaderThenGeneratesValidDotHeader) { + Graph testGraph{&ctx, true}; + + std::ostringstream dot; + exporter.writeHeader(dot); + std::string header = dot.str(); + + EXPECT_NE(header.find("digraph \"graph\" {"), std::string::npos); + EXPECT_NE(header.find("rankdir=TB;"), std::string::npos); + EXPECT_NE(header.find("nodesep=1;"), std::string::npos); + EXPECT_NE(header.find("ranksep=1;"), std::string::npos); + EXPECT_NE(header.find("node [shape=box, style=filled];"), std::string::npos); + EXPECT_NE(header.find("edge [color=black];"), std::string::npos); +} + +TEST_F(GraphDotExporterTest, GivenGraphWithCommandWhenWriteNodesThenGeneratesNodeDefinitions) { + Graph testGraph{&ctx, true}; + Mock event; + Mock cmdlist; + + testGraph.capture(&cmdlist, &event, 0U, nullptr); + testGraph.stopCapturing(); + + std::ostringstream dot; + exporter.writeNodes(dot, testGraph, 0, 0); + std::string output = dot.str(); + + EXPECT_NE(output.find("L0_S0_C0"), std::string::npos); + EXPECT_NE(output.find("zeCommandListAppendBarrier"), std::string::npos); +} + +TEST_F(GraphDotExporterTest, GivenGraphWithMultipleCommandsWhenWriteEdgesThenGeneratesSequentialEdges) { + Graph testGraph{&ctx, true}; + Mock event; + Mock cmdlist; + + testGraph.capture(&cmdlist, &event, 0U, nullptr); + testGraph.capture(&cmdlist, nullptr, nullptr, 0U, nullptr, 0U, nullptr); + testGraph.stopCapturing(); + + std::ostringstream dot; + exporter.writeEdges(dot, testGraph, 0, 0); + std::string output = dot.str(); + + EXPECT_NE(output.find("L0_S0_C0 -> L0_S0_C1"), std::string::npos); +} + +TEST_F(GraphDotExporterTest, GivenGraphWithCommandWhenGetCommandNodeLabelThenReturnsCorrectLabel) { + Graph testGraph{&ctx, true}; + Mock event; + Mock cmdlist; + + testGraph.capture(&cmdlist, &event, 0U, nullptr); + testGraph.stopCapturing(); + + std::string label = exporter.getCommandNodeLabel(testGraph, 0); + EXPECT_EQ(label, "zeCommandListAppendBarrier"); +} + +TEST_F(GraphDotExporterTest, GivenDifferentCommandTypesWhenGetCommandNodeAttributesThenReturnsCorrectColors) { + Graph testGraph{&ctx, true}; + Mock event; + Mock cmdlist; + + testGraph.capture(&cmdlist, &event, 0U, nullptr); + testGraph.capture(&cmdlist, nullptr, nullptr, 0U, nullptr, 0U, nullptr); + testGraph.capture(&cmdlist, &event); + + testGraph.stopCapturing(); + + EXPECT_EQ(exporter.getCommandNodeAttributes(testGraph, 0), ", fillcolor=orange"); + EXPECT_EQ(exporter.getCommandNodeAttributes(testGraph, 1), ", fillcolor=lightblue"); + EXPECT_EQ(exporter.getCommandNodeAttributes(testGraph, 2), ", fillcolor=yellow"); +} + +TEST_F(GraphDotExporterTest, WhenGenerateNodeIdThenReturnsCorrectFormat) { + EXPECT_EQ(exporter.generateNodeId(0, 0, 0), "L0_S0_C0"); + EXPECT_EQ(exporter.generateNodeId(1, 2, 3), "L1_S2_C3"); + EXPECT_EQ(exporter.generateNodeId(10, 20, 30), "L10_S20_C30"); +} + +TEST_F(GraphDotExporterTest, WhenGenerateSubgraphIdThenReturnsCorrectFormat) { + EXPECT_EQ(exporter.generateSubgraphId(0, 0), "L0_S0"); + EXPECT_EQ(exporter.generateSubgraphId(1, 2), "L1_S2"); + EXPECT_EQ(exporter.generateSubgraphId(10, 20), "L10_S20"); +} + +TEST_F(GraphDotExporterTest, WhenGetSubgraphFillColorThenReturnsCorrectColors) { + EXPECT_EQ(exporter.getSubgraphFillColor(1), "grey90"); + EXPECT_EQ(exporter.getSubgraphFillColor(2), "grey80"); + EXPECT_EQ(exporter.getSubgraphFillColor(3), "grey70"); + EXPECT_EQ(exporter.getSubgraphFillColor(4), "grey60"); + EXPECT_EQ(exporter.getSubgraphFillColor(5), "grey50"); +} + +TEST_F(GraphDotExporterTest, GivenDeepLevelWhenGetSubgraphFillColorThenReturnsDeepestColor) { + EXPECT_EQ(exporter.getSubgraphFillColor(6), "grey50"); + EXPECT_EQ(exporter.getSubgraphFillColor(10), "grey50"); + EXPECT_EQ(exporter.getSubgraphFillColor(100), "grey50"); +} + +TEST_F(GraphDotExporterTest, GivenGraphWithoutSubgraphsWhenWriteSubgraphsThenGeneratesNoOutput) { + Graph testGraph{&ctx, true}; + std::ostringstream dot; + exporter.writeSubgraphs(dot, testGraph, 0); + std::string output = dot.str(); + + EXPECT_TRUE(output.empty()); +} + +TEST_F(GraphDotExporterTest, GivenGraphWithSubgraphsWhenWriteSubgraphsThenGeneratesSubgraphStructure) { + Graph testGraph{&ctx, true}; + Mock forkEvent; + Mock joinEvent; + Mock mainCmdList; + Mock subCmdList; + + Graph *testGraphPtr = &testGraph; + captureCommand(mainCmdList, testGraphPtr, &mainCmdList, &forkEvent, 0U, nullptr); + + Graph *subGraph = nullptr; + testGraph.forkTo(subCmdList, subGraph, forkEvent); + ASSERT_NE(subGraph, nullptr); + + captureCommand(subCmdList, subGraph, &subCmdList, nullptr, nullptr, 0U, nullptr, 0U, nullptr); + captureCommand(subCmdList, subGraph, &subCmdList, &joinEvent, 0U, nullptr); + + testGraph.tryJoinOnNextCommand(subCmdList, joinEvent); + captureCommand(mainCmdList, testGraphPtr, &mainCmdList, nullptr, 0U, nullptr); + testGraph.stopCapturing(); + + std::ostringstream dot; + exporter.writeSubgraphs(dot, testGraph, 0); + std::string output = dot.str(); + + EXPECT_NE(output.find("// Subgraphs:"), std::string::npos); + EXPECT_NE(output.find("subgraph cluster_L1_S0"), std::string::npos); + EXPECT_NE(output.find("label=\"Subgraph 1-0\""), std::string::npos); + EXPECT_NE(output.find("style=filled"), std::string::npos); + EXPECT_NE(output.find("fillcolor=grey90"), std::string::npos); +} + +TEST_F(GraphDotExporterTest, GivenGraphWithNestedSubgraphsWhenWriteSubgraphsThenGeneratesNestedStructure) { + Graph testGraph{&ctx, true}; + Mock forkEvent1; + Mock forkEvent2; + Mock joinEvent1; + Mock joinEvent2; + Mock mainCmdList; + Mock subCmdList1; + Mock subCmdList2; + + Graph *testGraphPtr = &testGraph; + captureCommand(mainCmdList, testGraphPtr, &mainCmdList, &forkEvent1, 0U, nullptr); + + Graph *subGraph1 = nullptr; + testGraph.forkTo(subCmdList1, subGraph1, forkEvent1); + ASSERT_NE(subGraph1, nullptr); + + captureCommand(subCmdList1, subGraph1, &subCmdList1, nullptr, nullptr, 0U, nullptr, 0U, nullptr); + + Graph *subGraph2 = nullptr; + captureCommand(subCmdList1, subGraph1, &subCmdList1, &forkEvent2, 0U, nullptr); + subGraph1->forkTo(subCmdList2, subGraph2, forkEvent2); + ASSERT_NE(subGraph2, nullptr); + + captureCommand(subCmdList2, subGraph2, &subCmdList2, &joinEvent2, 0U, nullptr); + + subGraph1->tryJoinOnNextCommand(subCmdList2, joinEvent2); + captureCommand(subCmdList1, subGraph1, &subCmdList1, &joinEvent1, 0U, nullptr); + + testGraph.tryJoinOnNextCommand(subCmdList1, joinEvent1); + testGraph.stopCapturing(); + + std::ostringstream dot; + exporter.writeSubgraphs(dot, testGraph, 0); + std::string output = dot.str(); + + EXPECT_NE(output.find("subgraph cluster_L1_S0"), std::string::npos); + EXPECT_NE(output.find("subgraph cluster_L2_S0"), std::string::npos); + EXPECT_NE(output.find("fillcolor=grey90"), std::string::npos); + EXPECT_NE(output.find("fillcolor=grey80"), std::string::npos); +} + +TEST_F(GraphDotExporterTest, GivenGraphWithAdjacentSubgraphsWhenWriteSubgraphsThenGeneratesMultipleSubgraphs) { + Graph testGraph{&ctx, true}; + Mock forkEvent1; + Mock forkEvent2; + Mock joinEvent1; + Mock joinEvent2; + Mock mainCmdList; + Mock subCmdList1; + Mock subCmdList2; + + Graph *testGraphPtr = &testGraph; + captureCommand(mainCmdList, testGraphPtr, &mainCmdList, &forkEvent1, 0U, nullptr); + + Graph *subGraph1 = nullptr; + testGraph.forkTo(subCmdList1, subGraph1, forkEvent1); + ASSERT_NE(subGraph1, nullptr); + + captureCommand(subCmdList1, subGraph1, &subCmdList1, nullptr, nullptr, 0U, nullptr, 0U, nullptr); + captureCommand(subCmdList1, subGraph1, &subCmdList1, &joinEvent1, 0U, nullptr); + + testGraph.tryJoinOnNextCommand(subCmdList1, joinEvent1); + captureCommand(mainCmdList, testGraphPtr, &mainCmdList, &forkEvent2, 0U, nullptr); + + Graph *subGraph2 = nullptr; + testGraph.forkTo(subCmdList2, subGraph2, forkEvent2); + ASSERT_NE(subGraph2, nullptr); + + captureCommand(subCmdList2, subGraph2, &subCmdList2, nullptr, nullptr, 0U, nullptr, 0U, nullptr); + captureCommand(subCmdList2, subGraph2, &subCmdList2, &joinEvent2, 0U, nullptr); + + testGraph.tryJoinOnNextCommand(subCmdList2, joinEvent2); + captureCommand(mainCmdList, testGraphPtr, &mainCmdList, nullptr, 0U, nullptr); + testGraph.stopCapturing(); + + std::ostringstream dot; + exporter.writeSubgraphs(dot, testGraph, 0); + std::string output = dot.str(); + + EXPECT_NE(output.find("// Subgraphs:"), std::string::npos); + EXPECT_NE(output.find("subgraph cluster_L1_S0"), std::string::npos); + EXPECT_NE(output.find("subgraph cluster_L1_S1"), std::string::npos); + EXPECT_NE(output.find("label=\"Subgraph 1-0\""), std::string::npos); + EXPECT_NE(output.find("label=\"Subgraph 1-1\""), std::string::npos); +} + +TEST_F(GraphDotExporterTest, WhenFindSubgraphIndexWithInvalidSubgraphThenReturnsNullopt) { + const StackVec subGraphs; + Graph fakeSubgraph{&ctx, true}; + auto index = exporter.findSubgraphIndex(subGraphs, &fakeSubgraph); + + EXPECT_FALSE(index.has_value()); +} + +TEST_F(GraphDotExporterTest, WhenFindSubgraphIndexWithValidGraphThenReturnsCorrectIndex) { + Graph testGraph{&ctx, true}; + Mock forkEvent; + Mock joinEvent; + Mock mainCmdList; + Mock subCmdList; + + Graph *testGraphPtr = &testGraph; + captureCommand(mainCmdList, testGraphPtr, &mainCmdList, &forkEvent, 0U, nullptr); + + Graph *subGraph = nullptr; + testGraph.forkTo(subCmdList, subGraph, forkEvent); + ASSERT_NE(subGraph, nullptr); + + testGraph.tryJoinOnNextCommand(subCmdList, joinEvent); + captureCommand(mainCmdList, testGraphPtr, &mainCmdList, nullptr, 0U, nullptr); + testGraph.stopCapturing(); + + const auto &subGraphs = testGraph.getSubgraphs(); + auto index = exporter.findSubgraphIndex(subGraphs, subGraph); + + ASSERT_TRUE(index.has_value()); + EXPECT_EQ(index.value(), 0U); +} + +TEST_F(GraphDotExporterTest, WhenFindSubgraphIndexByCommandListWithInvalidCommandListThenReturnsNullopt) { + const StackVec subGraphs; + Mock fakeCmdList; + auto index = exporter.findSubgraphIndexByCommandList(subGraphs, &fakeCmdList); + + EXPECT_FALSE(index.has_value()); +} + +TEST_F(GraphDotExporterTest, GivenGraphWithEmptySubgraphWhenWriteForkJoinEdgesThenNoEdges) { + Graph testGraph{&ctx, true}; + Mock forkEvent; + Mock joinEvent; + Mock mainCmdList; + Mock subCmdList; + + Graph *testGraphPtr = &testGraph; + captureCommand(mainCmdList, testGraphPtr, &mainCmdList, &forkEvent, 0U, nullptr); + + Graph *subGraph = nullptr; + testGraph.forkTo(subCmdList, subGraph, forkEvent); + ASSERT_NE(subGraph, nullptr); + + testGraph.tryJoinOnNextCommand(subCmdList, joinEvent); + captureCommand(mainCmdList, testGraphPtr, &mainCmdList, nullptr, 0U, nullptr); + testGraph.stopCapturing(); + + std::ostringstream dot; + exporter.writeForkJoinEdges(dot, testGraph, 0, 0); + std::string output = dot.str(); + + EXPECT_EQ(output.find("->"), std::string::npos); +} + +TEST_F(GraphDotExporterTest, GivenGraphWithUnjoinedForksWhenWriteUnjoinedForkEdgesThenGeneratesUnjoinedEdges) { + Graph testGraph{&ctx, true}; + Mock forkEvent; + Mock mainCmdList; + Mock subCmdList; + + Graph *testGraphPtr = &testGraph; + captureCommand(mainCmdList, testGraphPtr, &mainCmdList, &forkEvent, 0U, nullptr); + + Graph *subGraph = nullptr; + testGraph.forkTo(subCmdList, subGraph, forkEvent); + ASSERT_NE(subGraph, nullptr); + + captureCommand(subCmdList, subGraph, &subCmdList, nullptr, nullptr, 0U, nullptr, 0U, nullptr); + + std::ostringstream dot; + exporter.writeUnjoinedForkEdges(dot, testGraph, 0, 0); + std::string output = dot.str(); + + EXPECT_NE(output.find("// Unjoined forks:"), std::string::npos); + EXPECT_NE(output.find("L0_S0_C0 -> L1_S0_C0 [color=red, label=\"unjoined fork\"];"), std::string::npos); + + // Prevent double free with unjoined forks + Mock joinEvent; + testGraph.tryJoinOnNextCommand(subCmdList, joinEvent); + captureCommand(mainCmdList, testGraphPtr, &mainCmdList, &forkEvent, 0U, nullptr); + testGraph.stopCapturing(); +} + +TEST_F(GraphDotExporterTest, GivenGraphWithEmptyUnjoinedSubgraphWhenWriteUnjoinedForkEdgesThenOutputIsEmpty) { + Graph testGraph{&ctx, true}; + Mock forkEvent; + Mock mainCmdList; + Mock subCmdList; + + Graph *testGraphPtr = &testGraph; + captureCommand(mainCmdList, testGraphPtr, &mainCmdList, &forkEvent, 0U, nullptr); + + Graph *subGraph = nullptr; + testGraph.forkTo(subCmdList, subGraph, forkEvent); + ASSERT_NE(subGraph, nullptr); + + std::ostringstream dot; + exporter.writeUnjoinedForkEdges(dot, testGraph, 0, 0); + std::string output = dot.str(); + + EXPECT_EQ(output.find("L0_S0_C0 -> L1_S0_C0 [color=red, label=\"unjoined fork\"];"), std::string::npos); + + // Prevent double free with unjoined forks + Mock joinEvent; + testGraph.tryJoinOnNextCommand(subCmdList, joinEvent); + captureCommand(mainCmdList, testGraphPtr, &mainCmdList, &forkEvent, 0U, nullptr); + testGraph.stopCapturing(); +} + +class GraphDotExporterFileTest : public GraphDotExporterTest { + protected: + void SetUp() override { + GraphDotExporterTest::SetUp(); + + fopenBackup = std::make_unique>(&NEO::IoFunctions::fopenPtr, NEO::IoFunctions::mockFopen); + fwriteBackup = std::make_unique>(&NEO::IoFunctions::fwritePtr, NEO::IoFunctions::mockFwrite); + fcloseBackup = std::make_unique>(&NEO::IoFunctions::fclosePtr, NEO::IoFunctions::mockFclose); + + mockFopenReturnedBackup = std::make_unique>(&IoFunctions::mockFopenReturned); + + mockFopenCalledBefore = NEO::IoFunctions::mockFopenCalled; + mockFwriteCalledBefore = NEO::IoFunctions::mockFwriteCalled; + mockFcloseCalledBefore = NEO::IoFunctions::mockFcloseCalled; + } + + void setupSuccessfulWrite(Graph &testGraph) { + std::string expectedContent = exporter.exportToString(testGraph); + ASSERT_NE(expectedContent.size(), 0U); + + mockFwriteReturnBackup = std::make_unique>(&NEO::IoFunctions::mockFwriteReturn, expectedContent.size()); + + if (expectedContent.size() > 0) { + buffer = std::make_unique(expectedContent.size() + 1); + memset(buffer.get(), 0, expectedContent.size() + 1); + mockFwriteBufferBackup = std::make_unique>(&NEO::IoFunctions::mockFwriteBuffer, buffer.get()); + } + } + + void setupFailedOpen() { + *mockFopenReturnedBackup = static_cast(nullptr); + } + + void setupFailedWrite() { + mockFwriteReturnBackup = std::make_unique>(&NEO::IoFunctions::mockFwriteReturn, static_cast(0)); + } + + std::string getWrittenContent() const { + return buffer ? std::string(buffer.get()) : std::string{}; + } + + std::unique_ptr> fopenBackup; + std::unique_ptr> fwriteBackup; + std::unique_ptr> fcloseBackup; + std::unique_ptr> mockFopenReturnedBackup; + std::unique_ptr> mockFwriteReturnBackup; + std::unique_ptr> mockFwriteBufferBackup; + std::unique_ptr buffer; + + uint32_t mockFopenCalledBefore; + uint32_t mockFwriteCalledBefore; + uint32_t mockFcloseCalledBefore; +}; + +TEST_F(GraphDotExporterFileTest, GivenEmptyGraphWhenExportToFileThenWritesValidDotContent) { + Graph testGraph{&ctx, true}; + setupSuccessfulWrite(testGraph); + + auto result = exporter.exportToFile(testGraph, testFilePath.c_str()); + EXPECT_EQ(ZE_RESULT_SUCCESS, result); + + std::string writtenContent = getWrittenContent(); + EXPECT_NE(writtenContent.find("digraph \"graph\" {"), std::string::npos); + EXPECT_NE(writtenContent.find('}'), std::string::npos); + + EXPECT_EQ(mockFopenCalledBefore + 1, NEO::IoFunctions::mockFopenCalled); + EXPECT_EQ(mockFwriteCalledBefore + 1, NEO::IoFunctions::mockFwriteCalled); + EXPECT_EQ(mockFcloseCalledBefore + 1, NEO::IoFunctions::mockFcloseCalled); +} + +TEST_F(GraphDotExporterFileTest, GivenFailedFileOpenWhenExportToFileThenReturnsUnknownError) { + Graph testGraph{&ctx, true}; + setupFailedOpen(); + + auto result = exporter.exportToFile(testGraph, testFilePath.c_str()); + EXPECT_EQ(ZE_RESULT_ERROR_UNKNOWN, result); + + EXPECT_EQ(mockFopenCalledBefore + 1, NEO::IoFunctions::mockFopenCalled); + EXPECT_EQ(mockFwriteCalledBefore, NEO::IoFunctions::mockFwriteCalled); +} + +TEST_F(GraphDotExporterFileTest, GivenFailedFileWriteWhenExportToFileThenReturnsUnknownError) { + Graph testGraph{&ctx, true}; + setupFailedWrite(); + + auto result = exporter.exportToFile(testGraph, testFilePath.c_str()); + EXPECT_EQ(ZE_RESULT_ERROR_UNKNOWN, result); + + EXPECT_EQ(mockFopenCalledBefore + 1, NEO::IoFunctions::mockFopenCalled); + EXPECT_EQ(mockFwriteCalledBefore + 1, NEO::IoFunctions::mockFwriteCalled); + EXPECT_EQ(mockFcloseCalledBefore + 1, NEO::IoFunctions::mockFcloseCalled); +} + +} // namespace ult +} // namespace L0 diff --git a/level_zero/experimental/source/graph/graph.cpp b/level_zero/experimental/source/graph/graph.cpp index a704645152..82c7203f30 100644 --- a/level_zero/experimental/source/graph/graph.cpp +++ b/level_zero/experimental/source/graph/graph.cpp @@ -7,11 +7,17 @@ #include "level_zero/experimental/source/graph/graph.h" +#include "shared/source/utilities/io_functions.h" + #include "level_zero/core/source/cmdlist/cmdlist.h" #include "level_zero/core/source/context/context.h" #include "level_zero/core/source/event/event.h" #include "level_zero/core/source/kernel/kernel_imp.h" +#include +#include +#include + namespace L0 { Graph::~Graph() { @@ -452,4 +458,256 @@ void recordHandleSignalEventFromPreviousCommand(L0::CommandList &srcCmdList, Gra captureTarget.registerSignallingEventFromPreviousCommand(*L0::Event::fromHandle(event)); } +ze_result_t GraphDotExporter::exportToFile(const Graph &graph, const char *filePath) const { + if (nullptr == filePath) { + return ZE_RESULT_ERROR_INVALID_ARGUMENT; + } + + FILE *file = NEO::IoFunctions::fopenPtr(filePath, "w"); + if (nullptr == file) { + return ZE_RESULT_ERROR_UNKNOWN; + } + + std::string dotContent = exportToString(graph); + size_t bytesWritten = NEO::IoFunctions::fwritePtr(dotContent.c_str(), 1, dotContent.size(), file); + NEO::IoFunctions::fclosePtr(file); + + if (bytesWritten != dotContent.size()) { + return ZE_RESULT_ERROR_UNKNOWN; + } + + return ZE_RESULT_SUCCESS; +} + +std::string GraphDotExporter::exportToString(const Graph &graph) const { + std::ostringstream dot; + + writeHeader(dot); + writeNodes(dot, graph, 0, 0); + writeEdges(dot, graph, 0, 0); + writeSubgraphs(dot, graph, 0); + + dot << "}\n"; + return dot.str(); +} + +void GraphDotExporter::writeHeader(std::ostringstream &dot) const { + dot << "digraph \"graph\" {\n"; + dot << " rankdir=TB;\n"; + dot << " nodesep=1;\n"; + dot << " ranksep=1;\n"; + dot << " node [shape=box, style=filled];\n"; + dot << " edge [color=black];\n\n"; +} + +void GraphDotExporter::writeNodes(std::ostringstream &dot, const Graph &graph, uint32_t level, uint32_t subgraphId) const { + const std::string indent(static_cast(level + 1) * 2, ' '); + dot << indent << "// Command nodes:\n"; + + const auto &commands = graph.getCapturedCommands(); + for (CapturedCommandId cmdId = 0; cmdId < static_cast(commands.size()); ++cmdId) { + const std::string nodeId = generateNodeId(level, subgraphId, cmdId); + const std::string label = getCommandNodeLabel(graph, cmdId); + const std::string attributes = getCommandNodeAttributes(graph, cmdId); + + dot << indent << nodeId << " [label=\"" << label << "\"" << attributes << "];\n"; + } + dot << "\n"; +} + +void GraphDotExporter::writeEdges(std::ostringstream &dot, const Graph &graph, uint32_t level, uint32_t subgraphId) const { + writeSequentialEdges(dot, graph, level, subgraphId); + writeForkJoinEdges(dot, graph, level, subgraphId); + writeUnjoinedForkEdges(dot, graph, level, subgraphId); + + dot << "\n"; +} + +void GraphDotExporter::writeSequentialEdges(std::ostringstream &dot, const Graph &graph, uint32_t level, uint32_t subgraphId) const { + const std::string indent(static_cast(level + 1) * 2, ' '); + + const auto &commands = graph.getCapturedCommands(); + dot << indent << "// Sequential edges:\n"; + + for (CapturedCommandId cmdId = 1; cmdId < static_cast(commands.size()); ++cmdId) { + const std::string fromNode = generateNodeId(level, subgraphId, cmdId - 1); + const std::string toNode = generateNodeId(level, subgraphId, cmdId); + dot << indent << fromNode << " -> " << toNode << ";\n"; + } +} + +void GraphDotExporter::writeForkJoinEdges(std::ostringstream &dot, const Graph &graph, uint32_t level, uint32_t subgraphId) const { + const std::string indent(static_cast(level + 1) * 2, ' '); + + const auto &joinedForks = graph.getJoinedForks(); + const auto &subGraphs = graph.getSubgraphs(); + + dot << "\n" + << indent << "// Fork/Join edges:\n"; + + for (const auto &[forkCmdId, forkJoinInfo] : joinedForks) { + const auto subgraphIndex = findSubgraphIndex(subGraphs, forkJoinInfo.forkDestiny); + if (subgraphIndex && !forkJoinInfo.forkDestiny->getCapturedCommands().empty()) { + const auto &subgraphCommands = forkJoinInfo.forkDestiny->getCapturedCommands(); + const std::string forkNode = generateNodeId(level, subgraphId, forkJoinInfo.forkCommandId); + const std::string subgraphFirstNode = generateNodeId(level + 1, *subgraphIndex, 0); + const std::string subgraphLastNode = generateNodeId(level + 1, *subgraphIndex, static_cast(subgraphCommands.size()) - 1); + const std::string joinNode = generateNodeId(level, subgraphId, forkJoinInfo.joinCommandId); + + dot << indent << forkNode << " -> " << subgraphFirstNode << ";\n"; + dot << indent << subgraphLastNode << " -> " << joinNode << ";\n"; + } + } +} + +void GraphDotExporter::writeUnjoinedForkEdges(std::ostringstream &dot, const Graph &graph, uint32_t level, uint32_t subgraphId) const { + const std::string indent(static_cast(level + 1) * 2, ' '); + + const auto &unjoinedForks = graph.getUnjoinedForks(); + const auto &subGraphs = graph.getSubgraphs(); + + dot << "\n" + << indent << "// Unjoined forks:\n"; + + for (const auto &[cmdList, forkInfo] : unjoinedForks) { + const auto subgraphIndex = findSubgraphIndexByCommandList(subGraphs, cmdList); + if (subgraphIndex && !subGraphs[*subgraphIndex]->getCapturedCommands().empty()) { + const std::string forkNode = generateNodeId(level, subgraphId, forkInfo.forkCommandId); + const std::string subgraphFirstNode = generateNodeId(level + 1, *subgraphIndex, 0); + dot << indent << forkNode << " -> " << subgraphFirstNode << " [color=red, label=\"unjoined fork\"];\n"; + } + } +} + +std::optional GraphDotExporter::findSubgraphIndex(const StackVec &subGraphs, const Graph *targetGraph) const { + for (uint32_t i = 0; i < static_cast(subGraphs.size()); ++i) { + if (subGraphs[i] == targetGraph) { + return i; + } + } + return std::nullopt; +} + +std::optional GraphDotExporter::findSubgraphIndexByCommandList(const StackVec &subGraphs, const L0::CommandList *cmdList) const { + for (uint32_t i = 0; i < static_cast(subGraphs.size()); ++i) { + if (subGraphs[i]->getExecutionTarget() == cmdList) { + return i; + } + } + return std::nullopt; +} + +void GraphDotExporter::writeSubgraphs(std::ostringstream &dot, const Graph &graph, uint32_t level) const { + const auto &subGraphs = graph.getSubgraphs(); + if (subGraphs.empty()) { + return; + } + + const std::string indent(static_cast(level + 1) * 2, ' '); + dot << indent << "// Subgraphs:\n"; + + for (uint32_t subgraphId = 0; subgraphId < static_cast(subGraphs.size()); ++subgraphId) { + const std::string clusterName = "cluster_" + generateSubgraphId(level + 1, subgraphId); + + dot << indent << "subgraph " << clusterName << " {\n"; + dot << indent << " label=\"Subgraph " << (level + 1) << "-" << subgraphId << "\";\n"; + dot << indent << " style=filled;\n"; + dot << indent << " fillcolor=" << getSubgraphFillColor(level + 1) << ";\n\n"; + + writeNodes(dot, *subGraphs[subgraphId], level + 1, subgraphId); + writeEdges(dot, *subGraphs[subgraphId], level + 1, subgraphId); + writeSubgraphs(dot, *subGraphs[subgraphId], level + 1); + + dot << indent << " }\n\n"; + } +} + +std::string GraphDotExporter::getCommandNodeLabel(const Graph &graph, CapturedCommandId cmdId) const { + const auto &commands = graph.getCapturedCommands(); + const auto &cmd = commands[cmdId]; + + std::string baseLabel; + switch (static_cast(cmd.index())) { +#define RR_CAPTURED_API(X) \ + case CaptureApi::X: \ + baseLabel = #X; \ + break; + + RR_CAPTURED_APIS() +#undef RR_CAPTURED_API + + default: + baseLabel = "Unknown"; + break; + } + + return baseLabel; +} + +std::string GraphDotExporter::getCommandNodeAttributes(const Graph &graph, CapturedCommandId cmdId) const { + const auto &commands = graph.getCapturedCommands(); + const auto &cmd = commands[cmdId]; + + switch (static_cast(cmd.index())) { + case CaptureApi::zeCommandListAppendMemoryCopy: + case CaptureApi::zeCommandListAppendMemoryCopyRegion: + case CaptureApi::zeCommandListAppendMemoryCopyFromContext: + case CaptureApi::zeCommandListAppendMemoryFill: + return ", fillcolor=lightblue"; + + case CaptureApi::zeCommandListAppendBarrier: + case CaptureApi::zeCommandListAppendMemoryRangesBarrier: + return ", fillcolor=orange"; + + case CaptureApi::zeCommandListAppendSignalEvent: + case CaptureApi::zeCommandListAppendWaitOnEvents: + case CaptureApi::zeCommandListAppendEventReset: + return ", fillcolor=yellow"; + + case CaptureApi::zeCommandListAppendImageCopy: + case CaptureApi::zeCommandListAppendImageCopyRegion: + case CaptureApi::zeCommandListAppendImageCopyToMemory: + case CaptureApi::zeCommandListAppendImageCopyFromMemory: + case CaptureApi::zeCommandListAppendImageCopyToMemoryExt: + case CaptureApi::zeCommandListAppendImageCopyFromMemoryExt: + return ", fillcolor=lightgreen"; + + case CaptureApi::zeCommandListAppendWriteGlobalTimestamp: + case CaptureApi::zeCommandListAppendQueryKernelTimestamps: + return ", fillcolor=pink"; + + default: + return ", fillcolor=aliceblue"; + } +} + +std::string GraphDotExporter::generateNodeId(uint32_t level, uint32_t subgraphId, CapturedCommandId cmdId) const { + std::ostringstream oss; + oss << "L" << level << "_S" << subgraphId << "_C" << cmdId; + return oss.str(); +} + +std::string GraphDotExporter::generateSubgraphId(uint32_t level, uint32_t subgraphId) const { + std::ostringstream oss; + oss << "L" << level << "_S" << subgraphId; + return oss.str(); +} + +std::string GraphDotExporter::getSubgraphFillColor(uint32_t level) const { + const std::vector colors = { + "grey90", // Level 1 + "grey80", // Level 2 + "grey70", // Level 3 + "grey60", // Level 4 + "grey50" // Level 5+ + }; + + size_t colorIndex = static_cast(level) - 1; + if (colorIndex >= colors.size()) { + colorIndex = colors.size() - 1; + } + + return colors[colorIndex]; +} + } // namespace L0 \ No newline at end of file diff --git a/level_zero/experimental/source/graph/graph.h b/level_zero/experimental/source/graph/graph.h index 1792e51923..0e19f87dca 100644 --- a/level_zero/experimental/source/graph/graph.h +++ b/level_zero/experimental/source/graph/graph.h @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -51,6 +52,22 @@ using ClosureVariants = std::variant< using CapturedCommand = ClosureVariants; using CapturedCommandId = uint32_t; + +struct Graph; + +struct ForkInfo { + CapturedCommandId forkCommandId = 0; + ze_event_handle_t forkEvent = nullptr; +}; + +struct ForkJoinInfo { + CapturedCommandId forkCommandId = 0; + CapturedCommandId joinCommandId = 0; + ze_event_handle_t forkEvent = nullptr; + ze_event_handle_t joinEvent = nullptr; + Graph *forkDestiny = nullptr; +}; + struct Graph : _ze_graph_handle_t { Graph(L0::Context *ctx, bool preallocated) : ctx(ctx), preallocated(preallocated) { commands.reserve(16); @@ -85,10 +102,22 @@ struct Graph : _ze_graph_handle_t { return ZE_RESULT_SUCCESS; } - const std::vector &getCapturedCommands() { + const std::vector &getCapturedCommands() const { return commands; } + const StackVec &getSubgraphs() const { + return subGraphs; + } + + const std::unordered_map &getJoinedForks() const { + return joinedForks; + } + + const std::unordered_map &getUnjoinedForks() const { + return unjoinedForks; + } + Graph *getJoinedForkTarget(CapturedCommandId cmdId) { auto it = joinedForks.find(cmdId); if (joinedForks.end() == it) { @@ -165,21 +194,7 @@ struct Graph : _ze_graph_handle_t { bool wasCapturingStopped = false; std::unordered_map recordedSignals; - - struct ForkInfo { - CapturedCommandId forkCommandId = 0; - ze_event_handle_t forkEvent = nullptr; - }; - std::unordered_map unjoinedForks; - - struct ForkJoinInfo { - CapturedCommandId forkCommandId = 0; - CapturedCommandId joinCommandId = 0; - ze_event_handle_t forkEvent = nullptr; - ze_event_handle_t joinEvent = nullptr; - Graph *forkDestiny = nullptr; - }; std::unordered_map joinedForks; }; @@ -272,6 +287,31 @@ struct ExecutableGraph : _ze_executable_graph_handle_t { GraphSubmissionChain submissionChain; }; +class GraphDotExporter { + public: + ze_result_t exportToFile(const Graph &graph, const char *filePath) const; + + protected: + std::string exportToString(const Graph &graph) const; + + void writeHeader(std::ostringstream &dot) const; + void writeNodes(std::ostringstream &dot, const Graph &graph, uint32_t level, uint32_t subgraphId) const; + void writeSubgraphs(std::ostringstream &dot, const Graph &graph, uint32_t level) const; + void writeEdges(std::ostringstream &dot, const Graph &graph, uint32_t level, uint32_t subgraphId) const; + void writeSequentialEdges(std::ostringstream &dot, const Graph &graph, uint32_t level, uint32_t subgraphId) const; + void writeForkJoinEdges(std::ostringstream &dot, const Graph &graph, uint32_t level, uint32_t subgraphId) const; + void writeUnjoinedForkEdges(std::ostringstream &dot, const Graph &graph, uint32_t level, uint32_t subgraphId) const; + + std::optional findSubgraphIndex(const StackVec &subGraphs, const Graph *targetGraph) const; + std::optional findSubgraphIndexByCommandList(const StackVec &subGraphs, const L0::CommandList *cmdList) const; + + std::string getCommandNodeLabel(const Graph &graph, CapturedCommandId cmdId) const; + std::string getCommandNodeAttributes(const Graph &graph, CapturedCommandId cmdId) const; + std::string generateNodeId(uint32_t level, uint32_t subgraphId, CapturedCommandId cmdId) const; + std::string generateSubgraphId(uint32_t level, uint32_t subgraphId) const; + std::string getSubgraphFillColor(uint32_t level) const; +}; + constexpr size_t maxVariantSize = 2 * 64; #define RR_CAPTURED_API(X) \ static_assert(sizeof(Closure) <= maxVariantSize, #X " is too big for common variant. Please export some of its state to ClosureExternalStorage"); diff --git a/shared/source/utilities/io_functions.h b/shared/source/utilities/io_functions.h index cc8792659f..4ab8cbb412 100644 --- a/shared/source/utilities/io_functions.h +++ b/shared/source/utilities/io_functions.h @@ -30,7 +30,7 @@ using fseekFuncPtr = int (*)(FILE *, long int, int); using ftellFuncPtr = long int (*)(FILE *); using rewindFuncPtr = decltype(&rewind); using freadFuncPtr = size_t (*)(void *, size_t, size_t, FILE *); -using fwriteFuncPtr = decltype(&fwrite); +using fwriteFuncPtr = size_t (*)(const void *, size_t, size_t, FILE *); using fflushFuncPtr = decltype(&fflush); using mkdirFuncPtr = int (*)(const char *);