Files
TheRock/rocm-libraries/projects/hipdnn/tests/frontend/IntegrationGraphLifting.cpp
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

566 lines
25 KiB
C++

// Copyright © Advanced Micro Devices, Inc., or its affiliates.
// SPDX-License-Identifier: MIT
#include <algorithm>
#include <cstdint>
#include <gtest/gtest.h>
#include <hip/hip_runtime.h>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include <hipdnn_data_sdk/utilities/Tensor.hpp>
#include <hipdnn_data_sdk/utilities/Workspace.hpp>
#include <hipdnn_frontend.hpp>
#include <hipdnn_test_sdk/constants/ConvFpropConstants.hpp>
#include <hipdnn_test_sdk/utilities/IntegrationTestFixture.hpp>
#include <hipdnn_test_sdk/utilities/LiftingTestHelpers.hpp>
#include <hipdnn_test_sdk/utilities/TestUtilities.hpp>
#include <hipdnn_test_sdk/utilities/TestableGraph.hpp>
#include <hipdnn_test_sdk/utilities/ToVec.hpp>
using namespace hipdnn_frontend;
using namespace hipdnn_frontend::graph;
using hipdnn_tests::toVec;
using namespace hipdnn_tests::constants;
using hipdnn_tests::buildConvFpropGraph;
using hipdnn_tests::IntegrationTestFixture;
using hipdnn_tests::liftGraph;
using hipdnn_tests::liftGraphWithoutFinalization;
using hipdnn_tests::TestableGraphLifting;
namespace
{
// Builds a conv fprop graph via the frontend, lowers it through the backend C-API
// via build_operation_graph(), then lifts it back with fromBackendDescriptor()
// and verifies the reconstructed graph matches the original.
class IntegrationGraphLifting : public IntegrationTestFixture
{
};
// Runs execute() on a conv-fprop graph (built via buildConvFpropGraph, with the
// fixed X/W/Y UIDs and dims) over zero-initialized device buffers. The fake
// TestGoodPlugin records the execute entry without computing numbers, so this
// only proves the graph+plan object reaches the execute path. Tensor/Workspace
// RAII own the device memory. Returns the Error from Graph::execute.
hipdnn_frontend::Error executeConvFpropOnDeviceBundle(const hipdnn_frontend::graph::Graph& graph,
hipdnnHandle_t handle)
{
using hipdnn_data_sdk::utilities::Tensor;
using hipdnn_data_sdk::utilities::Workspace;
Tensor<float> xTensor(toVec(K_FPROP_TENSOR_X_DIMS));
Tensor<float> wTensor(toVec(K_FPROP_TENSOR_W_DIMS));
Tensor<float> yTensor(toVec(K_FPROP_TENSOR_Y_DIMS));
xTensor.fillWithValue(0.0F);
wTensor.fillWithValue(0.0F);
yTensor.fillWithValue(0.0F);
int64_t workspaceSize = 0;
auto wsResult = graph.get_workspace_size(workspaceSize);
if(wsResult.code != ErrorCode::OK)
{
return wsResult;
}
const Workspace workspace(static_cast<size_t>(std::max<int64_t>(workspaceSize, 0)));
std::unordered_map<int64_t, void*> variantPack;
variantPack[K_FPROP_TENSOR_X_UID] = xTensor.memory().deviceData();
variantPack[K_FPROP_TENSOR_W_UID] = wTensor.memory().deviceData();
variantPack[K_FPROP_TENSOR_Y_UID] = yTensor.memory().deviceData();
return graph.execute(handle, variantPack, workspace.get());
}
// Builds a conv fprop graph, lowers via build_operation_graph(handle), extracts the
// raw descriptor, creates a new graph with fromBackendDescriptor(), and verifies
// tensor dimensions, data types, convolution parameters, and graph-level data types.
TEST_F(IntegrationGraphLifting, ConvFpropRoundTripViaCApi)
{
auto originalGraph = buildConvFpropGraph();
auto liftedGraph = liftGraph(*originalGraph, _handle);
ASSERT_NE(liftedGraph, nullptr);
// Verify graph name survives the C-API round-trip
EXPECT_EQ(liftedGraph->get_name(), "ConvFpropTestGraph");
// Verify graph-level data types
EXPECT_EQ(liftedGraph->get_compute_data_type(), DataType::FLOAT);
EXPECT_EQ(liftedGraph->get_intermediate_data_type(), DataType::FLOAT);
EXPECT_EQ(liftedGraph->get_io_data_type(), DataType::FLOAT);
// Verify tensors by UID
auto tensorMap = liftedGraph->getTensorsByUid();
ASSERT_EQ(tensorMap.size(), 3u)
<< "Expected 3 tensors (X, W, Y) in lifted graph"; // NOLINT(readability-implicit-bool-conversion)
// Verify X tensor
ASSERT_NE(tensorMap.count(K_FPROP_TENSOR_X_UID), 0u);
auto liftedX = tensorMap[K_FPROP_TENSOR_X_UID];
EXPECT_EQ(liftedX->get_dim(), toVec(K_FPROP_TENSOR_X_DIMS));
EXPECT_EQ(liftedX->get_stride(), toVec(K_FPROP_TENSOR_X_STRIDES));
EXPECT_EQ(liftedX->get_data_type(), DataType::FLOAT);
// Verify W tensor
ASSERT_NE(tensorMap.count(K_FPROP_TENSOR_W_UID), 0u);
auto liftedW = tensorMap[K_FPROP_TENSOR_W_UID];
EXPECT_EQ(liftedW->get_dim(), toVec(K_FPROP_TENSOR_W_DIMS));
EXPECT_EQ(liftedW->get_stride(), toVec(K_FPROP_TENSOR_W_STRIDES));
EXPECT_EQ(liftedW->get_data_type(), DataType::FLOAT);
// Verify Y tensor
ASSERT_NE(tensorMap.count(K_FPROP_TENSOR_Y_UID), 0u);
auto liftedY = tensorMap[K_FPROP_TENSOR_Y_UID];
EXPECT_EQ(liftedY->get_data_type(), DataType::FLOAT);
EXPECT_EQ(liftedY->get_dim(), toVec(K_FPROP_TENSOR_Y_DIMS));
EXPECT_EQ(liftedY->get_stride(), toVec(K_FPROP_TENSOR_Y_STRIDES));
// Verify the lifted graph has the correct number of sub-nodes
auto& subNodes = liftedGraph->getSubNodes();
ASSERT_EQ(subNodes.size(), 1u)
<< "Expected 1 operation node in lifted graph"; // NOLINT(readability-implicit-bool-conversion)
// Access the conv fprop node and verify convolution parameters
auto* convNode = dynamic_cast<ConvolutionFpropNode*>(subNodes[0].get());
ASSERT_NE(convNode, nullptr)
<< "Expected a ConvolutionFpropNode"; // NOLINT(readability-implicit-bool-conversion)
EXPECT_EQ(convNode->attributes.get_pre_padding(), toVec(K_FPROP_CONV_PADDING));
EXPECT_EQ(convNode->attributes.get_post_padding(), toVec(K_FPROP_CONV_PADDING));
EXPECT_EQ(convNode->attributes.get_stride(), toVec(K_FPROP_CONV_STRIDE));
EXPECT_EQ(convNode->attributes.get_dilation(), toVec(K_FPROP_CONV_DILATION));
EXPECT_EQ(convNode->attributes.get_convolution_mode(), ConvolutionMode::CROSS_CORRELATION);
EXPECT_EQ(convNode->attributes.get_name(), "conv_fprop_op");
}
// Verifies that tensors are accessible by UID on the reconstructed graph,
// confirming tensor identity is preserved through the round-trip.
TEST_F(IntegrationGraphLifting, ConvFpropTensorSharingPreserved)
{
auto originalGraph = buildConvFpropGraph();
auto liftedGraph = liftGraph(*originalGraph, _handle);
ASSERT_NE(liftedGraph, nullptr);
// All tensors should be accessible by UID
auto tensorMap = liftedGraph->getTensorsByUid();
EXPECT_NE(tensorMap.count(K_FPROP_TENSOR_X_UID), 0u) << "X tensor not found by UID";
EXPECT_NE(tensorMap.count(K_FPROP_TENSOR_W_UID), 0u) << "W tensor not found by UID";
EXPECT_NE(tensorMap.count(K_FPROP_TENSOR_Y_UID), 0u) << "Y tensor not found by UID";
// Verify the node references the same tensor objects via UID
auto& subNodes = liftedGraph->getSubNodes();
ASSERT_EQ(subNodes.size(), 1u);
auto* convNode = dynamic_cast<ConvolutionFpropNode*>(subNodes[0].get());
ASSERT_NE(convNode, nullptr);
EXPECT_EQ(convNode->attributes.get_x()->get_uid(), K_FPROP_TENSOR_X_UID);
EXPECT_EQ(convNode->attributes.get_w()->get_uid(), K_FPROP_TENSOR_W_UID);
EXPECT_EQ(convNode->attributes.get_y()->get_uid(), K_FPROP_TENSOR_Y_UID);
// Verify tensor objects are shared between the tensorMap and conv node attributes
EXPECT_EQ(tensorMap[K_FPROP_TENSOR_X_UID].get(), convNode->attributes.get_x().get());
EXPECT_EQ(tensorMap[K_FPROP_TENSOR_W_UID].get(), convNode->attributes.get_w().get());
EXPECT_EQ(tensorMap[K_FPROP_TENSOR_Y_UID].get(), convNode->attributes.get_y().get());
}
// Builds a graph with set_preferred_engine_id_ext(42), lowers, lifts, and verifies
// that get_preferred_engine_id_ext() returns 42 on the reconstructed graph.
TEST_F(IntegrationGraphLifting, PreferredEngineIdPreservedThroughCApi)
{
auto originalGraph = buildConvFpropGraph();
constexpr int64_t K_PREFERRED_ENGINE_ID = 42;
originalGraph->set_preferred_engine_id_ext(K_PREFERRED_ENGINE_ID);
auto liftedGraph = liftGraph(*originalGraph, _handle);
ASSERT_NE(liftedGraph, nullptr);
auto liftedEngineId = liftedGraph->get_preferred_engine_id_ext();
ASSERT_TRUE(liftedEngineId.has_value())
<< "Preferred engine ID should be set after lifting"; // NOLINT(readability-implicit-bool-conversion)
EXPECT_EQ(liftedEngineId.value(), K_PREFERRED_ENGINE_ID);
}
// Verifies that fromBackendDescriptor(nullptr) returns an error.
TEST_F(IntegrationGraphLifting, NullDescriptorReturnsError)
{
auto graph = std::make_shared<TestableGraphLifting>();
auto result = graph->fromBackendDescriptor(nullptr);
EXPECT_EQ(result.code, ErrorCode::INVALID_VALUE)
<< "fromBackendDescriptor(nullptr) should return INVALID_VALUE"; // NOLINT(readability-implicit-bool-conversion)
}
// Builds a graph with FLOAT compute, HALF intermediate, and BFLOAT16 io data types,
// lowers through the C-API, lifts, and verifies all three are preserved.
TEST_F(IntegrationGraphLifting, DataTypesPreservedThroughCApi)
{
auto originalGraph = buildConvFpropGraph(
"ConvFpropTestGraph", DataType::FLOAT, DataType::HALF, DataType::BFLOAT16);
auto liftedGraph = liftGraph(*originalGraph, _handle);
ASSERT_NE(liftedGraph, nullptr);
EXPECT_EQ(liftedGraph->get_compute_data_type(), DataType::FLOAT);
EXPECT_EQ(liftedGraph->get_intermediate_data_type(), DataType::HALF);
EXPECT_EQ(liftedGraph->get_io_data_type(), DataType::BFLOAT16);
}
// Builds a conv fprop graph, serializes to binary, creates a backend descriptor
// from bytes (no handle, no finalize), calls fromBackendDescriptor(), and verifies
// the reconstructed graph matches the original.
TEST_F(IntegrationGraphLifting, ConvFpropLiftWithoutFinalization)
{
auto originalGraph = buildConvFpropGraph();
auto liftedGraph = liftGraphWithoutFinalization(*originalGraph);
ASSERT_NE(liftedGraph, nullptr);
// Verify graph-level data types
EXPECT_EQ(liftedGraph->get_compute_data_type(), DataType::FLOAT);
EXPECT_EQ(liftedGraph->get_intermediate_data_type(), DataType::FLOAT);
EXPECT_EQ(liftedGraph->get_io_data_type(), DataType::FLOAT);
// Verify the lifted graph has 1 operation node
auto& subNodes = liftedGraph->getSubNodes();
ASSERT_EQ(subNodes.size(), 1u);
auto* convNode = dynamic_cast<ConvolutionFpropNode*>(subNodes[0].get());
ASSERT_NE(convNode, nullptr);
// Verify convolution parameters
EXPECT_EQ(convNode->attributes.get_pre_padding(), toVec(K_FPROP_CONV_PADDING));
EXPECT_EQ(convNode->attributes.get_post_padding(), toVec(K_FPROP_CONV_PADDING));
EXPECT_EQ(convNode->attributes.get_stride(), toVec(K_FPROP_CONV_STRIDE));
EXPECT_EQ(convNode->attributes.get_dilation(), toVec(K_FPROP_CONV_DILATION));
EXPECT_EQ(convNode->attributes.get_convolution_mode(), ConvolutionMode::CROSS_CORRELATION);
// Verify tensor dims and strides
auto tensorMap = liftedGraph->getTensorsByUid();
ASSERT_EQ(tensorMap.size(), 3u);
EXPECT_EQ(tensorMap[K_FPROP_TENSOR_X_UID]->get_dim(), toVec(K_FPROP_TENSOR_X_DIMS));
EXPECT_EQ(tensorMap[K_FPROP_TENSOR_X_UID]->get_stride(), toVec(K_FPROP_TENSOR_X_STRIDES));
EXPECT_EQ(tensorMap[K_FPROP_TENSOR_W_UID]->get_dim(), toVec(K_FPROP_TENSOR_W_DIMS));
EXPECT_EQ(tensorMap[K_FPROP_TENSOR_W_UID]->get_stride(), toVec(K_FPROP_TENSOR_W_STRIDES));
}
// Exercises the deserialize() path with a handle (full finalization).
// Builds a conv fprop graph, serializes to binary, then uses deserialize()
// with a handle and verifies the reconstructed graph.
TEST_F(IntegrationGraphLifting, DeserializeViaBackendWithHandle)
{
auto originalGraph = buildConvFpropGraph();
auto result = originalGraph->validate();
ASSERT_EQ(result.code, ErrorCode::OK) << result.err_msg;
auto [data, serErr] = originalGraph->to_binary();
ASSERT_TRUE(serErr.is_good()) << serErr.get_message();
// Create a new graph and use deserialize with handle
auto liftedGraph = std::make_shared<TestableGraphLifting>();
result = liftedGraph->deserialize(_handle, data);
ASSERT_EQ(result.code, ErrorCode::OK) << result.err_msg;
// Verify graph-level data types
EXPECT_EQ(liftedGraph->get_compute_data_type(), DataType::FLOAT);
EXPECT_EQ(liftedGraph->get_intermediate_data_type(), DataType::FLOAT);
EXPECT_EQ(liftedGraph->get_io_data_type(), DataType::FLOAT);
// Verify the lifted graph has 1 operation node
auto& subNodes = liftedGraph->getSubNodes();
ASSERT_EQ(subNodes.size(), 1u);
auto* convNode = dynamic_cast<ConvolutionFpropNode*>(subNodes[0].get());
ASSERT_NE(convNode, nullptr);
// Verify convolution parameters
EXPECT_EQ(convNode->attributes.get_pre_padding(), toVec(K_FPROP_CONV_PADDING));
EXPECT_EQ(convNode->attributes.get_post_padding(), toVec(K_FPROP_CONV_PADDING));
EXPECT_EQ(convNode->attributes.get_stride(), toVec(K_FPROP_CONV_STRIDE));
EXPECT_EQ(convNode->attributes.get_dilation(), toVec(K_FPROP_CONV_DILATION));
// Verify tensor dims
auto tensorMap = liftedGraph->getTensorsByUid();
ASSERT_EQ(tensorMap.size(), 3u);
EXPECT_EQ(tensorMap[K_FPROP_TENSOR_X_UID]->get_dim(), toVec(K_FPROP_TENSOR_X_DIMS));
EXPECT_EQ(tensorMap[K_FPROP_TENSOR_W_UID]->get_dim(), toVec(K_FPROP_TENSOR_W_DIMS));
}
// Exercises the deserialize() path without a handle (no finalization).
// Verifies that the graph can be reconstructed from binary data without
// requiring a hipdnnHandle_t.
TEST_F(IntegrationGraphLifting, DeserializeViaBackendWithoutHandle)
{
auto originalGraph = buildConvFpropGraph();
auto result = originalGraph->validate();
ASSERT_EQ(result.code, ErrorCode::OK) << result.err_msg;
auto [data, serErr] = originalGraph->to_binary();
ASSERT_TRUE(serErr.is_good()) << serErr.get_message();
// Create a new graph and use deserialize without handle
auto liftedGraph = std::make_shared<TestableGraphLifting>();
result = liftedGraph->deserialize(nullptr, data);
ASSERT_EQ(result.code, ErrorCode::OK) << result.err_msg;
// Verify graph-level data types
EXPECT_EQ(liftedGraph->get_compute_data_type(), DataType::FLOAT);
EXPECT_EQ(liftedGraph->get_intermediate_data_type(), DataType::FLOAT);
EXPECT_EQ(liftedGraph->get_io_data_type(), DataType::FLOAT);
// Verify tensors are present
auto tensorMap = liftedGraph->getTensorsByUid();
ASSERT_EQ(tensorMap.size(), 3u);
ASSERT_NE(tensorMap.count(K_FPROP_TENSOR_X_UID), 0u);
EXPECT_EQ(tensorMap[K_FPROP_TENSOR_X_UID]->get_dim(), toVec(K_FPROP_TENSOR_X_DIMS));
EXPECT_EQ(tensorMap[K_FPROP_TENSOR_X_UID]->get_stride(), toVec(K_FPROP_TENSOR_X_STRIDES));
ASSERT_NE(tensorMap.count(K_FPROP_TENSOR_W_UID), 0u);
EXPECT_EQ(tensorMap[K_FPROP_TENSOR_W_UID]->get_dim(), toVec(K_FPROP_TENSOR_W_DIMS));
EXPECT_EQ(tensorMap[K_FPROP_TENSOR_W_UID]->get_stride(), toVec(K_FPROP_TENSOR_W_STRIDES));
ASSERT_NE(tensorMap.count(K_FPROP_TENSOR_Y_UID), 0u);
// Verify conv node
auto& subNodes = liftedGraph->getSubNodes();
ASSERT_EQ(subNodes.size(), 1u);
auto* convNode = dynamic_cast<ConvolutionFpropNode*>(subNodes[0].get());
ASSERT_NE(convNode, nullptr);
EXPECT_EQ(convNode->attributes.get_pre_padding(), toVec(K_FPROP_CONV_PADDING));
EXPECT_EQ(convNode->attributes.get_stride(), toVec(K_FPROP_CONV_STRIDE));
}
// Verifies that fromBackendDescriptor returns an error (not a crash) when given
// a graph descriptor with no operations set.
TEST_F(IntegrationGraphLifting, EmptyGraphDescriptorReturnsError)
{
// Create a backend graph descriptor with no operations
hipdnnBackendDescriptor_t desc = nullptr;
ASSERT_EQ(hipdnnBackendCreateDescriptor(HIPDNN_BACKEND_OPERATIONGRAPH_DESCRIPTOR, &desc),
HIPDNN_STATUS_SUCCESS);
// Attempt to lift — should return an error since no operations are set
auto graph = std::make_shared<TestableGraphLifting>();
auto result = graph->fromBackendDescriptor(desc);
EXPECT_NE(result.code, ErrorCode::OK)
<< "fromBackendDescriptor should fail on a descriptor "
"with no operations"; // NOLINT(readability-implicit-bool-conversion)
hipdnnBackendDestroyDescriptor(desc);
}
// Verifies that deserialize returns an error (not a crash) when
// given corrupt (garbage) bytes.
TEST_F(IntegrationGraphLifting, DeserializeViaBackendCorruptDataReturnsError)
{
const std::vector<uint8_t> garbage = {0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01, 0x02, 0x03};
auto graph = std::make_shared<TestableGraphLifting>();
auto result = graph->deserialize(_handle, garbage);
EXPECT_NE(result.code, ErrorCode::OK)
<< "deserialize should fail on corrupt data"; // NOLINT(readability-implicit-bool-conversion)
}
// Verifies that deserialize returns an error (not a crash) when
// given an empty data vector.
TEST_F(IntegrationGraphLifting, DeserializeViaBackendEmptyDataReturnsError)
{
const std::vector<uint8_t> empty;
auto graph = std::make_shared<TestableGraphLifting>();
auto result = graph->deserialize(_handle, empty);
EXPECT_NE(result.code, ErrorCode::OK)
<< "deserialize should fail on empty data"; // NOLINT(readability-implicit-bool-conversion)
}
// Verifies that the graph name survives the C-API round-trip (lower -> lift).
TEST_F(IntegrationGraphLifting, GraphNamePreservedThroughCApi)
{
auto originalGraph = buildConvFpropGraph("LiftingTestGraph");
auto liftedGraph = liftGraph(*originalGraph, _handle);
ASSERT_NE(liftedGraph, nullptr);
EXPECT_EQ(liftedGraph->get_name(), "LiftingTestGraph");
}
// Exercises the deserialize() path and verifies the graph name
// is preserved through the deserialization path.
TEST_F(IntegrationGraphLifting, GraphNamePreservedThroughDeserializeViaBackend)
{
auto originalGraph = buildConvFpropGraph("LiftingTestGraph");
auto result = originalGraph->validate();
ASSERT_EQ(result.code, ErrorCode::OK) << result.err_msg;
auto [data, serErr] = originalGraph->to_binary();
ASSERT_TRUE(serErr.is_good()) << serErr.get_message();
// Create a new graph and use deserialize without handle
auto liftedGraph = std::make_shared<TestableGraphLifting>();
result = liftedGraph->deserialize(nullptr, data);
ASSERT_EQ(result.code, ErrorCode::OK) << result.err_msg;
EXPECT_EQ(liftedGraph->get_name(), "LiftingTestGraph");
}
// Exercises the deserialize() path and verifies the preferred engine ID
// is preserved through the deserialization path.
TEST_F(IntegrationGraphLifting, PreferredEngineIdPreservedThroughDeserialize)
{
auto originalGraph = buildConvFpropGraph();
originalGraph->set_preferred_engine_id_ext(42);
auto result = originalGraph->validate();
ASSERT_EQ(result.code, ErrorCode::OK) << result.err_msg;
auto [data, serErr] = originalGraph->to_binary();
ASSERT_TRUE(serErr.is_good()) << serErr.get_message();
// Create a new graph and use deserialize without handle
auto liftedGraph = std::make_shared<TestableGraphLifting>();
result = liftedGraph->deserialize(nullptr, data);
ASSERT_EQ(result.code, ErrorCode::OK) << result.err_msg;
EXPECT_EQ(liftedGraph->get_preferred_engine_id_ext(), 42);
}
// Exercises the JSON serialize/deserialize path with a handle (full finalization)
// for a conv fprop graph.
TEST_F(IntegrationGraphLifting, JsonRoundTripWithHandle)
{
auto originalGraph = buildConvFpropGraph();
auto result = originalGraph->validate();
ASSERT_EQ(result.code, ErrorCode::OK) << result.err_msg;
// Serialize to JSON (auto-lowers internally)
std::string jsonData;
result = originalGraph->serialize(jsonData);
ASSERT_EQ(result.code, ErrorCode::OK) << result.err_msg;
ASSERT_FALSE(jsonData.empty());
// Deserialize from JSON with handle
auto liftedGraph = std::make_shared<TestableGraphLifting>();
result = liftedGraph->deserialize(_handle, jsonData);
ASSERT_EQ(result.code, ErrorCode::OK) << result.err_msg;
// Verify graph-level attributes
EXPECT_EQ(liftedGraph->get_name(), "ConvFpropTestGraph");
EXPECT_EQ(liftedGraph->get_compute_data_type(), DataType::FLOAT);
EXPECT_EQ(liftedGraph->get_intermediate_data_type(), DataType::FLOAT);
EXPECT_EQ(liftedGraph->get_io_data_type(), DataType::FLOAT);
// Verify tensors by UID
auto tensorMap = liftedGraph->getTensorsByUid();
ASSERT_EQ(tensorMap.size(), 3u) << "Expected 3 tensors (X, W, Y)";
hipdnn_tests::verifyTensorInGraph(tensorMap,
K_FPROP_TENSOR_X_UID,
"X",
toVec(K_FPROP_TENSOR_X_DIMS),
toVec(K_FPROP_TENSOR_X_STRIDES),
DataType::FLOAT);
hipdnn_tests::verifyTensorInGraph(tensorMap,
K_FPROP_TENSOR_W_UID,
"W",
toVec(K_FPROP_TENSOR_W_DIMS),
toVec(K_FPROP_TENSOR_W_STRIDES),
DataType::FLOAT);
hipdnn_tests::verifyTensorInGraph(tensorMap,
K_FPROP_TENSOR_Y_UID,
"Y",
toVec(K_FPROP_TENSOR_Y_DIMS),
toVec(K_FPROP_TENSOR_Y_STRIDES),
DataType::FLOAT);
// Verify sub-node count and type
auto& subNodes = liftedGraph->getSubNodes();
ASSERT_EQ(subNodes.size(), 1u) << "Expected 1 operation node in lifted graph";
auto* convNode = dynamic_cast<ConvolutionFpropNode*>(subNodes[0].get());
ASSERT_NE(convNode, nullptr) << "Expected a ConvolutionFpropNode";
// Verify convolution parameters
EXPECT_EQ(convNode->attributes.get_pre_padding(), toVec(K_FPROP_CONV_PADDING));
EXPECT_EQ(convNode->attributes.get_post_padding(), toVec(K_FPROP_CONV_PADDING));
EXPECT_EQ(convNode->attributes.get_stride(), toVec(K_FPROP_CONV_STRIDE));
EXPECT_EQ(convNode->attributes.get_dilation(), toVec(K_FPROP_CONV_DILATION));
EXPECT_EQ(convNode->attributes.get_convolution_mode(), ConvolutionMode::CROSS_CORRELATION);
EXPECT_EQ(convNode->attributes.get_name(), "conv_fprop_op");
}
// build()s a conv-fprop graph (finalizing a plan), serializes graph+plan to one
// blob, then deserializes with a handle. Verifies the graph is reconstructed
// (1 ConvolutionFpropNode, 3 tensors), the plan is re-attached, and the object
// reaches the execute path. No numerical check here; that gate lives in the GPU
// test (IntegrationConvForwardSerializeRoundTrip.cpp).
TEST_F(IntegrationGraphLifting, GraphPlusPlanWithHandleAttachAndExecuteReachesPlugin)
{
auto originalGraph = buildConvFpropGraph();
auto result = originalGraph->build(_handle);
ASSERT_EQ(result.code, ErrorCode::OK) << result.err_msg;
auto [data, serErr] = originalGraph->to_binary();
ASSERT_TRUE(serErr.is_good()) << serErr.get_message();
auto lifted = std::make_shared<TestableGraphLifting>();
result = lifted->deserialize(_handle, data);
ASSERT_EQ(result.code, ErrorCode::OK) << result.err_msg;
// Graph reconstructed.
auto& subNodes = lifted->getSubNodes();
ASSERT_EQ(subNodes.size(), 1u) << "Expected 1 operation node in lifted graph";
auto* convNode = dynamic_cast<ConvolutionFpropNode*>(subNodes[0].get());
ASSERT_NE(convNode, nullptr) << "Expected a ConvolutionFpropNode";
auto tensorMap = lifted->getTensorsByUid();
ASSERT_EQ(tensorMap.size(), 3u) << "Expected 3 tensors (X, W, Y) in lifted graph";
// Plan re-attached.
EXPECT_TRUE(lifted->hasExecutionPlan())
<< "Execution plan should be attached after deserialize with a handle";
auto execResult = executeConvFpropOnDeviceBundle(*lifted, _handle);
EXPECT_EQ(execResult.code, ErrorCode::OK) << execResult.err_msg;
}
// validate()s a conv-fprop graph but does NOT build it, so to_binary() yields
// the legacy plan-less blob. After deserialize the graph has no plan; a fresh
// build() creates one and the graph executes. Confirms the no-plan path is
// unchanged.
TEST_F(IntegrationGraphLifting, BuildSerializeWithoutPlanThenFreshBuildExecutes)
{
auto originalGraph = buildConvFpropGraph();
auto result = originalGraph->validate();
ASSERT_EQ(result.code, ErrorCode::OK) << result.err_msg;
auto [data, serErr] = originalGraph->to_binary();
ASSERT_TRUE(serErr.is_good()) << serErr.get_message();
auto lifted = std::make_shared<TestableGraphLifting>();
result = lifted->deserialize(_handle, data);
ASSERT_EQ(result.code, ErrorCode::OK) << result.err_msg;
// No plan in the serialized blob.
EXPECT_FALSE(lifted->hasExecutionPlan())
<< "No execution plan should be present after deserializing a no-plan blob";
// A fresh build creates a new plan; the graph then executes.
result = lifted->build(_handle);
ASSERT_EQ(result.code, ErrorCode::OK) << result.err_msg;
EXPECT_TRUE(lifted->hasExecutionPlan())
<< "Execution plan should be present after a fresh build";
auto execResult = executeConvFpropOnDeviceBundle(*lifted, _handle);
EXPECT_EQ(execResult.code, ErrorCode::OK) << execResult.err_msg;
}
} // namespace