Files
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

915 lines
32 KiB
C++

// Copyright © Advanced Micro Devices, Inc., or its affiliates.
// SPDX-License-Identifier: MIT
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <hipdnn_data_sdk/logging/Logger.hpp>
#include <hipdnn_data_sdk/utilities/ShapeUtilities.hpp>
#include <hipdnn_data_sdk/utilities/Tensor.hpp>
#include <hipdnn_test_sdk/utilities/LogRecorder.hpp>
using namespace hipdnn_data_sdk::utilities;
struct ExtractStrideOrderTestCase
{
std::vector<int64_t> strides;
std::vector<int64_t> order;
bool log;
std::vector<int64_t> dim;
};
// Outside anonymous namespace so ADL finds it for gtest printing
// NOLINTNEXTLINE(misc-use-internal-linkage)
std::ostream& operator<<(std::ostream& os, const ExtractStrideOrderTestCase& tc)
{
os << "ExtractStrideOrderTestCase(";
os << " strides: ";
os << vecToString(tc.strides);
os << " order: ";
os << vecToString(tc.order);
os << " log: " << tc.log;
os << " )";
return os;
}
using TestShapeUtilsExtractStrideOrder = ::testing::TestWithParam<ExtractStrideOrderTestCase>;
TEST(TestShapeUtils, GenerateStridesNhwcValid)
{
const std::vector<int64_t> dim = {1, 2, 3, 4};
const std::vector<int64_t> strideOrder = {3, 0, 2, 1}; // NHWC
auto strides = generateStrides(dim, strideOrder);
EXPECT_EQ(strides, (std::vector<int64_t>{24, 1, 8, 2}));
}
TEST(TestShapeUtils, GenerateStridesNdhwcValid)
{
const std::vector<int64_t> dim = {1, 2, 3, 4, 5};
const std::vector<int64_t> strideOrder = {4, 0, 3, 2, 1}; // NDHWC
auto strides = generateStrides(dim, strideOrder);
EXPECT_EQ(strides, (std::vector<int64_t>{120, 1, 40, 10, 2}));
}
TEST(TestShapeUtils, GenerateStridesNchwValid)
{
const std::vector<int64_t> dim = {1, 2, 3, 4};
const std::vector<int64_t> strideOrder = {3, 2, 1, 0}; // NCHW
auto strides = generateStrides(dim, strideOrder);
EXPECT_EQ(strides, (std::vector<int64_t>{24, 12, 4, 1}));
}
TEST(TestShapeUtils, GenerateStridesNcdhwValid)
{
const std::vector<int64_t> dim = {1, 2, 3, 4, 5};
const std::vector<int64_t> strideOrder = {4, 3, 2, 1, 0}; // NCDHW
auto strides = generateStrides(dim, strideOrder);
EXPECT_EQ(strides, (std::vector<int64_t>{120, 60, 20, 5, 1}));
}
TEST(TestShapeUtils, GenerateStridesBhsdValid)
{
// BHSD: dims [B=2, H=4, S=128, D=64], row-major (same stride order as NCHW)
const std::vector<int64_t> dim = {2, 4, 128, 64};
auto strides = generateStrides(dim, TensorLayout::BHSD.strideOrder);
// D contiguous, then S, then H, then B
EXPECT_EQ(strides, (std::vector<int64_t>{32768, 8192, 64, 1}));
// BHSD should produce the same strides as NCHW
auto nchwStrides = generateStrides(dim, TensorLayout::NCHW.strideOrder);
EXPECT_EQ(strides, nchwStrides);
}
TEST(TestShapeUtils, GenerateStridesBshdValid)
{
// BSHD: dims [B=2, H=4, S=128, D=64], sequence-major
const std::vector<int64_t> dim = {2, 4, 128, 64};
auto strides = generateStrides(dim, TensorLayout::BSHD.strideOrder);
// D contiguous, then H, then S, then B
EXPECT_EQ(strides, (std::vector<int64_t>{32768, 64, 256, 1}));
// BSHD should NOT produce the same strides as NHWC
auto nhwcStrides = generateStrides(dim, TensorLayout::NHWC.strideOrder);
EXPECT_NE(strides, nhwcStrides);
}
TEST(TestShapeUtils, GenerateStridesSingleDimension)
{
const std::vector<int64_t> dim = {5};
const std::vector<int64_t> strideOrder = {0};
auto strides = generateStrides(dim, strideOrder);
EXPECT_EQ(strides, (std::vector<int64_t>{1}));
}
TEST(TestShapeUtils, StrideOrderNhwcFiveDimensions)
{
const size_t numDims = 5;
auto strideOrder = strideOrderNhwc(numDims);
EXPECT_EQ(strideOrder, (std::vector<int64_t>{4, 0, 3, 2, 1}));
}
TEST(TestShapeUtils, StrideOrderNhwcFourDimensions)
{
const size_t numDims = 4;
auto strideOrder = strideOrderNhwc(numDims);
EXPECT_EQ(strideOrder, (std::vector<int64_t>{3, 0, 2, 1}));
}
TEST(TestShapeUtils, StrideOrderNhwcThreeDimensions)
{
const size_t numDims = 3;
auto strideOrder = strideOrderNhwc(numDims);
EXPECT_EQ(strideOrder, (std::vector<int64_t>{2, 0, 1}));
}
TEST(TestShapeUtils, StrideOrderNhwcWithTwoDimensions)
{
const size_t numDims = 2;
auto strideOrder = strideOrderNhwc(numDims);
EXPECT_EQ(strideOrder, (std::vector<int64_t>{1, 0}));
}
TEST(TestShapeUtils, StrideOrderNhwcWithSingleDimension)
{
const size_t numDims = 1;
auto strideOrder = strideOrderNhwc(numDims);
EXPECT_EQ(strideOrder, (std::vector<int64_t>{0}));
}
TEST(TestShapeUtils, GenerateStridesEmptyDimensions)
{
const std::vector<int64_t> dim = {};
const std::vector<int64_t> strideOrder = {};
auto strides = generateStrides(dim, strideOrder);
EXPECT_EQ(strides, (std::vector<int64_t>{}));
}
TEST(TestShapeUtils, BroadcastCompatibleExactMatch)
{
const std::vector<int64_t> inputDims = {2, 3, 4};
const std::vector<int64_t> outputDims = {2, 3, 4};
EXPECT_TRUE(areDimensionsBroadcastCompatible(inputDims, outputDims));
}
TEST(TestShapeUtils, BroadcastCompatibleInputNon1OutputIs1Invalid)
{
// Input has non-1 value where output has 1 - invalid
const std::vector<int64_t> inputDims = {2, 3, 4};
const std::vector<int64_t> outputDims = {2, 1, 4};
EXPECT_FALSE(areDimensionsBroadcastCompatible(inputDims, outputDims));
}
TEST(TestShapeUtils, BroadcastCompatibleInputIs1OutputNon1Valid)
{
// Input has 1 where output has non-1 - valid (broadcast)
const std::vector<int64_t> inputDims = {2, 1, 4};
const std::vector<int64_t> outputDims = {2, 3, 4};
EXPECT_TRUE(areDimensionsBroadcastCompatible(inputDims, outputDims));
}
TEST(TestShapeUtils, BroadcastCompatibleFewerInputDims)
{
// Input has fewer dimensions - valid (implicit leading 1s)
const std::vector<int64_t> inputDims = {3, 4};
const std::vector<int64_t> outputDims = {2, 3, 4};
EXPECT_TRUE(areDimensionsBroadcastCompatible(inputDims, outputDims));
}
TEST(TestShapeUtils, BroadcastCompatibleMoreInputDimsInvalid)
{
// Input has more dimensions than output - invalid
const std::vector<int64_t> inputDims = {2, 3, 4, 5};
const std::vector<int64_t> outputDims = {3, 4, 5};
EXPECT_FALSE(areDimensionsBroadcastCompatible(inputDims, outputDims));
}
TEST(TestShapeUtils, BroadcastCompatibleMismatchedDimsInvalid)
{
// Input and output don't match (3 vs 5) - invalid
const std::vector<int64_t> inputDims = {2, 3, 4};
const std::vector<int64_t> outputDims = {2, 5, 4};
EXPECT_FALSE(areDimensionsBroadcastCompatible(inputDims, outputDims));
}
TEST(TestShapeUtils, BroadcastCompatibleEmptyInputValid)
{
// Empty input is broadcastable to any output
const std::vector<int64_t> inputDims = {};
const std::vector<int64_t> outputDims = {2, 3, 4};
EXPECT_TRUE(areDimensionsBroadcastCompatible(inputDims, outputDims));
}
TEST(TestShapeUtils, BroadcastCompatibleEmptyOutputWithNonEmptyInputInvalid)
{
// Non-empty input cannot broadcast to empty output
const std::vector<int64_t> inputDims = {2, 3};
const std::vector<int64_t> outputDims = {};
EXPECT_FALSE(areDimensionsBroadcastCompatible(inputDims, outputDims));
}
TEST(TestShapeUtils, BroadcastCompatibleBothEmpty)
{
// Both empty is valid (exact match)
const std::vector<int64_t> inputDims = {};
const std::vector<int64_t> outputDims = {};
EXPECT_TRUE(areDimensionsBroadcastCompatible(inputDims, outputDims));
}
TEST(TestShapeUtils, BroadcastCompatibleScalarToTensor)
{
// Scalar [1] broadcasts to any shape
const std::vector<int64_t> inputDims = {1};
const std::vector<int64_t> outputDims = {2, 3, 4};
EXPECT_TRUE(areDimensionsBroadcastCompatible(inputDims, outputDims));
}
TEST(TestShapeUtils, BroadcastCompatibleVectorToMatrix)
{
// Vector [4] broadcasts to matrix [3, 4]
const std::vector<int64_t> inputDims = {4};
const std::vector<int64_t> outputDims = {3, 4};
EXPECT_TRUE(areDimensionsBroadcastCompatible(inputDims, outputDims));
}
TEST(TestShapeUtils, BroadcastCompatibleVectorMismatchInvalid)
{
// Vector [5] cannot broadcast to matrix [3, 4]
const std::vector<int64_t> inputDims = {5};
const std::vector<int64_t> outputDims = {3, 4};
EXPECT_FALSE(areDimensionsBroadcastCompatible(inputDims, outputDims));
}
TEST(TestShapeUtils, BroadcastCompatibleComplexBroadcast)
{
// Complex broadcast: [1, 3, 1] -> [2, 3, 4]
const std::vector<int64_t> inputDims = {1, 3, 1};
const std::vector<int64_t> outputDims = {2, 3, 4};
EXPECT_TRUE(areDimensionsBroadcastCompatible(inputDims, outputDims));
}
TEST(TestShapeUtils, BroadcastCompatiblePartialMatchWithOnes)
{
// [2, 1, 4] -> [2, 3, 4]
const std::vector<int64_t> inputDims = {2, 1, 4};
const std::vector<int64_t> outputDims = {2, 3, 4};
EXPECT_TRUE(areDimensionsBroadcastCompatible(inputDims, outputDims));
}
TEST(TestShapeUtils, BroadcastCompatibleHigherDimensionalBroadcast)
{
// 5D broadcast: [1, 1, 3, 1, 5] -> [2, 4, 3, 6, 5]
const std::vector<int64_t> inputDims = {1, 1, 3, 1, 5};
const std::vector<int64_t> outputDims = {2, 4, 3, 6, 5};
EXPECT_TRUE(areDimensionsBroadcastCompatible(inputDims, outputDims));
}
TEST(TestShapeUtils, BroadcastCompatibleLeadingOnesImplicit)
{
// [3, 4] broadcasts to [1, 2, 3, 4] (implicit leading 1s)
const std::vector<int64_t> inputDims = {3, 4};
const std::vector<int64_t> outputDims = {1, 2, 3, 4};
EXPECT_TRUE(areDimensionsBroadcastCompatible(inputDims, outputDims));
}
TEST(TestShapeUtils, BroadcastCompatibleSingleDimToMultiDim)
{
// Single dimension [5] to multi-dimensional [2, 3, 5]
const std::vector<int64_t> inputDims = {5};
const std::vector<int64_t> outputDims = {2, 3, 5};
EXPECT_TRUE(areDimensionsBroadcastCompatible(inputDims, outputDims));
}
TEST(TestShapeUtils, BroadcastCompatibleAllOnesInput)
{
// All 1s input broadcasts to any matching rank output
const std::vector<int64_t> inputDims = {1, 1, 1};
const std::vector<int64_t> outputDims = {5, 7, 9};
EXPECT_TRUE(areDimensionsBroadcastCompatible(inputDims, outputDims));
}
TEST(TestShapeUtils, BroadcastCompatibleMixedOnesAndMatches)
{
// Mix of exact matches and 1s: [2, 1, 4, 1] -> [2, 3, 4, 5]
const std::vector<int64_t> inputDims = {2, 1, 4, 1};
const std::vector<int64_t> outputDims = {2, 3, 4, 5};
EXPECT_TRUE(areDimensionsBroadcastCompatible(inputDims, outputDims));
}
TEST(TestShapeUtils, GenerateStridesMoreDimsThanStridesThrows)
{
const std::vector<int64_t> dim = {1, 2, 3, 4};
const std::vector<int64_t> strideOrder = {2, 1, 0};
EXPECT_THROW(generateStrides(dim, strideOrder), std::invalid_argument);
}
TEST(TestShapeUtils, GenerateStridesLessDimsThanStridesValid)
{
const std::vector<int64_t> dim = {1, 2, 3};
const std::vector<int64_t> strideOrder = {3, 2, 1, 0};
auto strides = generateStrides(dim, strideOrder);
EXPECT_EQ(strides, (std::vector<int64_t>{6, 3, 1}));
}
TEST(TestShapeUtils, GenerateStridesFewerDimsNhwcOrdering)
{
const std::vector<int64_t> dim = {2, 3, 4};
const std::vector<int64_t> strideOrder = {2, 0, 1, 3};
// Should use first 3 elements: {2, 0, 1} for NHW ordering
auto strides = generateStrides(dim, strideOrder);
EXPECT_EQ(strides, (std::vector<int64_t>{12, 1, 3}));
}
TEST(TestShapeUtils, GenerateStridesEmptyDimsWithNonEmptyStridesValid)
{
const std::vector<int64_t> dim = {};
const std::vector<int64_t> strideOrder = {3, 2, 1, 0};
auto strides = generateStrides(dim, strideOrder);
EXPECT_EQ(strides, (std::vector<int64_t>{}));
}
TEST(TestShapeUtils, GenerateDefaultPackedStridesEmpty)
{
const std::vector<int64_t> dims = {};
auto strides = generateStrides(dims);
EXPECT_EQ(strides, (std::vector<int64_t>{}));
}
TEST(TestShapeUtils, GenerateDefaultPackedStrides1D)
{
const std::vector<int64_t> dims = {10};
auto strides = generateStrides(dims);
EXPECT_EQ(strides, (std::vector<int64_t>{1}));
}
TEST(TestShapeUtils, GenerateDefaultPackedStrides2D)
{
const std::vector<int64_t> dims = {5, 8};
auto strides = generateStrides(dims);
// Row-major: {8, 1}
EXPECT_EQ(strides, (std::vector<int64_t>{8, 1}));
}
TEST(TestShapeUtils, GenerateDefaultPackedStrides3D)
{
const std::vector<int64_t> dims = {4, 5, 6};
auto strides = generateStrides(dims);
// Row-major: {5*6, 6, 1} = {30, 6, 1}
EXPECT_EQ(strides, (std::vector<int64_t>{30, 6, 1}));
}
TEST(TestShapeUtils, GenerateDefaultPackedStrides4D)
{
const std::vector<int64_t> dims = {2, 3, 4, 5};
auto strides = generateStrides(dims);
// Row-major: {3*4*5, 4*5, 5, 1} = {60, 20, 5, 1}
EXPECT_EQ(strides, (std::vector<int64_t>{60, 20, 5, 1}));
}
TEST(TestShapeUtils, GenerateDefaultPackedStrides5D)
{
const std::vector<int64_t> dims = {2, 3, 4, 5, 6};
auto strides = generateStrides(dims);
// Row-major: {3*4*5*6, 4*5*6, 5*6, 6, 1} = {360, 120, 30, 6, 1}
EXPECT_EQ(strides, (std::vector<int64_t>{360, 120, 30, 6, 1}));
}
TEST(TestShapeUtils, GenerateDefaultPackedStrides6D)
{
const std::vector<int64_t> dims = {1, 2, 3, 4, 5, 6};
auto strides = generateStrides(dims);
// Row-major: {2*3*4*5*6, 3*4*5*6, 4*5*6, 5*6, 6, 1} = {720, 360, 120, 30, 6, 1}
EXPECT_EQ(strides, (std::vector<int64_t>{720, 360, 120, 30, 6, 1}));
}
TEST(TestShapeUtils, GenerateDefaultPackedStrides7D)
{
const std::vector<int64_t> dims = {2, 2, 2, 2, 2, 2, 2};
auto strides = generateStrides(dims);
// Row-major: {64, 32, 16, 8, 4, 2, 1}
EXPECT_EQ(strides, (std::vector<int64_t>{64, 32, 16, 8, 4, 2, 1}));
}
TEST(TestShapeUtils, GenerateDefaultPackedStridesWithOnes)
{
const std::vector<int64_t> dims = {1, 1, 5, 1, 3};
auto strides = generateStrides(dims);
// Row-major: {1*5*1*3, 5*1*3, 1*3, 3, 1} = {15, 15, 3, 3, 1}
EXPECT_EQ(strides, (std::vector<int64_t>{15, 15, 3, 3, 1}));
}
TEST(TestShapeUtils, GenerateDefaultPackedStridesLargeDims)
{
const std::vector<int64_t> dims = {100, 200};
auto strides = generateStrides(dims);
EXPECT_EQ(strides, (std::vector<int64_t>{200, 1}));
}
TEST(TestShapeUtils, GenerateDefaultPackedStridesMatchesNchwFor4D)
{
const std::vector<int64_t> dims = {2, 3, 4, 5};
auto packedStrides = generateStrides(dims);
auto nchwStrides = generateStrides(dims, {3, 2, 1, 0}); // NCHW stride order
EXPECT_EQ(packedStrides, nchwStrides);
}
TEST(TestShapeUtils, GenerateDefaultPackedStridesMatchesNcdhwFor5D)
{
const std::vector<int64_t> dims = {2, 3, 4, 5, 6};
auto packedStrides = generateStrides(dims);
auto ncdhwStrides = generateStrides(dims, {4, 3, 2, 1, 0}); // NCDHW stride order
EXPECT_EQ(packedStrides, ncdhwStrides);
}
TEST(TestShapeUtils, GenerateDefaultPackedStridesCalculation)
{
// Test the actual calculation logic
std::vector<int64_t> dims = {3, 4, 5};
auto strides = generateStrides(dims);
// Verify each stride is product of dimensions after it
EXPECT_EQ(strides[0], dims[1] * dims[2]); // 4 * 5 = 20
EXPECT_EQ(strides[1], dims[2]); // 5
EXPECT_EQ(strides[2], 1); // 1 (always for last dimension)
}
TEST(TestShapeUtils, GenerateDefaultPackedStridesAllOnes)
{
const std::vector<int64_t> dims = {1, 1, 1, 1};
auto strides = generateStrides(dims);
EXPECT_EQ(strides, (std::vector<int64_t>{1, 1, 1, 1}));
}
TEST_P(TestShapeUtilsExtractStrideOrder, VerifyExtractedStrideOrder)
{
auto& testCase = GetParam();
// Create RAII log recorder - captures all logs for this test
auto recorder
= hipdnn_test_sdk::utilities::SharedLogRecorder::withOverrideLevel(HIPDNN_SEV_INFO);
// Sanity-check test data
EXPECT_EQ(generateStrides(testCase.dim, testCase.order), testCase.strides);
const std::vector<int64_t> deducedStrideOrder = extractStrideOrder(testCase.strides);
EXPECT_EQ(deducedStrideOrder, testCase.order);
if(testCase.log)
{
// Verify we got exactly one warning log
ASSERT_EQ(recorder.getRecordedLogCount(), 1) << "Expected 1 log, but captured:\n"
<< recorder.getRecordedLogsAsString();
// Build expected warning message
const std::string expectedLogSuffix
= "extractStrideOrder(): Stride lengths " + vecToString(testCase.strides)
+ " are not unique, the deduced stride order " + vecToString(deducedStrideOrder)
+ " may not be correct.";
// Verify log contains expected message at WARN level
EXPECT_TRUE(recorder.hasLogContaining(HIPDNN_SEV_WARN, expectedLogSuffix))
<< "Expected log containing: \"" << expectedLogSuffix << "\"\n"
<< "But captured:\n"
<< recorder.getRecordedLogsAsString();
}
else
{
// Verify no logs were emitted
EXPECT_EQ(recorder.getRecordedLogCount(), 0) << "Expected no logs, but captured:\n"
<< recorder.getRecordedLogsAsString();
}
}
INSTANTIATE_TEST_SUITE_P(
,
TestShapeUtilsExtractStrideOrder,
::testing::Values(
// NHWC tests
ExtractStrideOrderTestCase{{24, 1, 8, 2}, {3, 0, 2, 1}, false, {10, 2, 3, 4}},
ExtractStrideOrderTestCase{{2, 1, 2, 1}, {3, 0, 2, 1}, true, {10, 1, 1, 2}},
ExtractStrideOrderTestCase{{2, 1, 2, 2}, {3, 0, 2, 1}, true, {10, 2, 1, 1}},
// NDHWC tests
ExtractStrideOrderTestCase{{120, 1, 40, 10, 2}, {4, 0, 3, 2, 1}, false, {10, 2, 3, 4, 5}},
ExtractStrideOrderTestCase{{4, 1, 4, 4, 4}, {4, 0, 3, 2, 1}, true, {10, 4, 1, 1, 1}},
ExtractStrideOrderTestCase{{4, 1, 4, 1, 1}, {4, 0, 3, 2, 1}, true, {10, 1, 1, 4, 1}},
ExtractStrideOrderTestCase{{2, 1, 2, 2, 1}, {4, 0, 3, 2, 1}, true, {10, 1, 1, 1, 2}},
// NCHW tests
ExtractStrideOrderTestCase{{24, 12, 4, 1}, {3, 2, 1, 0}, false, {10, 2, 3, 4}},
ExtractStrideOrderTestCase{{4, 4, 1, 1}, {3, 2, 1, 0}, true, {10, 1, 4, 1}},
ExtractStrideOrderTestCase{{4, 4, 4, 1}, {3, 2, 1, 0}, true, {10, 1, 1, 4}},
// NCDHW tests
ExtractStrideOrderTestCase{{120, 60, 20, 5, 1}, {4, 3, 2, 1, 0}, false, {1, 2, 3, 4, 5}},
ExtractStrideOrderTestCase{{4, 4, 4, 1, 1}, {4, 3, 2, 1, 0}, true, {10, 1, 1, 4, 1}},
ExtractStrideOrderTestCase{{4, 4, 1, 1, 1}, {4, 3, 2, 1, 0}, true, {10, 1, 4, 1, 1}},
ExtractStrideOrderTestCase{{4, 4, 4, 4, 1}, {4, 3, 2, 1, 0}, true, {10, 1, 1, 1, 4}},
// All ones --> NC...W
ExtractStrideOrderTestCase{{1, 1, 1, 1}, {3, 2, 1, 0}, true, {1, 1, 1, 1}},
ExtractStrideOrderTestCase{{1, 1, 1, 1, 1}, {4, 3, 2, 1, 0}, true, {1, 1, 1, 1, 1}},
// Uncommon
ExtractStrideOrderTestCase{{}, {}, false, {}},
ExtractStrideOrderTestCase{{1}, {0}, false, {1}},
ExtractStrideOrderTestCase{{1, 1}, {1, 0}, true, {1, 1}},
ExtractStrideOrderTestCase{{2, 1}, {1, 0}, false, {1, 2}},
ExtractStrideOrderTestCase{{1, 2}, {0, 1}, false, {2, 1}},
ExtractStrideOrderTestCase{{1, 1, 1}, {2, 1, 0}, true, {1, 1, 1}},
ExtractStrideOrderTestCase{{4, 2, 1}, {2, 1, 0}, false, {1, 2, 2}},
ExtractStrideOrderTestCase{{4, 1, 2}, {2, 0, 1}, false, {1, 2, 2}},
ExtractStrideOrderTestCase{{1, 2, 4}, {0, 1, 2}, false, {2, 2, 1}},
ExtractStrideOrderTestCase{{1, 1, 2}, {1, 0, 2}, true, {2, 1, 1}},
ExtractStrideOrderTestCase{{2, 1, 2}, {2, 0, 1}, true, {1, 2, 1}}, // NWC
ExtractStrideOrderTestCase{{2, 1, 1}, {2, 1, 0}, true, {1, 2, 1}}, // NCW
ExtractStrideOrderTestCase{{1, 2, 4, 8}, {0, 1, 2, 3}, false, {2, 2, 2, 1}},
ExtractStrideOrderTestCase{{1, 4, 2, 2}, {0, 3, 2, 1}, true, {2, 1, 2, 1}},
ExtractStrideOrderTestCase{{1, 1, 4, 8}, {1, 0, 2, 3}, true, {4, 1, 2, 1}},
ExtractStrideOrderTestCase{{1, 4, 8, 1}, {1, 2, 3, 0}, true, {4, 2, 1, 1}},
ExtractStrideOrderTestCase{{1, 4, 2, 16, 8}, {0, 2, 1, 4, 3}, false, {2, 2, 2, 1, 2}},
ExtractStrideOrderTestCase{{1, 4, 2, 2, 2}, {0, 4, 3, 2, 1}, true, {2, 1, 2, 1, 1}},
ExtractStrideOrderTestCase{{1, 1, 1, 4, 8}, {2, 1, 0, 3, 4}, true, {4, 1, 1, 2, 1}},
ExtractStrideOrderTestCase{{1, 4, 8, 8, 1}, {1, 2, 4, 3, 0}, true, {4, 2, 1, 1, 1}},
ExtractStrideOrderTestCase{
{1, 4, 2, 32, 16, 8}, {0, 2, 1, 5, 4, 3}, false, {2, 2, 2, 1, 2, 2}},
ExtractStrideOrderTestCase{
{1, 4, 2, 2, 2, 2}, {0, 5, 4, 3, 2, 1}, true, {2, 1, 2, 1, 1, 1}},
ExtractStrideOrderTestCase{
{1, 1, 1, 4, 4, 8}, {2, 1, 0, 4, 3, 5}, true, {4, 1, 1, 2, 1, 1}},
ExtractStrideOrderTestCase{
{1, 4, 8, 8, 8, 1}, {1, 2, 5, 4, 3, 0}, true, {4, 2, 1, 1, 1, 1}}));
TEST(TestShapeUtils, GenerateStridesWithPackedAxisNchwAxis1)
{
// NCHW input, make axis 1 (C) most packed
auto strides
= generateStridesWithPackedAxis({65536, 1024, 32, 1}, {2, 64, 32, 32}, {2, 64, 32, 32}, 1);
// Sorted by stride ascending: [3,2,1,0], rotate axis=1 to front → [1,0,3,2]
// strideOrder=[1,0,3,2]: dim1 gets stride 1
EXPECT_EQ(strides, (std::vector<int64_t>{64, 1, 4096, 128}));
}
TEST(TestShapeUtils, GenerateStridesWithPackedAxisNhwcAxis3)
{
// NHWC input, make axis 3 (W) most packed
auto strides
= generateStridesWithPackedAxis({65536, 1, 2048, 64}, {2, 64, 32, 32}, {2, 64, 32, 32}, 3);
EXPECT_EQ(strides, (std::vector<int64_t>{1024, 2048, 32, 1}));
}
TEST(TestShapeUtils, GenerateStridesWithPackedAxisNoAxisPreservesOrder)
{
// Without axis, stride ordering matches the reference layout
auto strides
= generateStridesWithPackedAxis({65536, 1024, 32, 1}, {2, 64, 32, 32}, {2, 64, 32, 32});
EXPECT_EQ(strides, (std::vector<int64_t>{65536, 1024, 32, 1}));
}
TEST(TestShapeUtils, GenerateStridesWithPackedAxisDifferentTargetDims)
{
// Target dims differ from reference (e.g., scale tensor with reduced axis dim)
auto strides
= generateStridesWithPackedAxis({65536, 1024, 32, 1}, {2, 64, 32, 32}, {2, 2, 32, 32}, 1);
EXPECT_EQ(strides, (std::vector<int64_t>{2, 1, 128, 4}));
}
TEST(TestShapeUtils, GenerateStridesWithPackedAxisSingletonTiebreaker)
{
// Singletons with equal strides should sort before non-singletons
// dims {1, 4, 1, 4} with strides {4, 1, 4, 1}: dims 0 and 2 are singletons with stride 4
auto strides = generateStridesWithPackedAxis({4, 1, 4, 1}, {1, 4, 1, 4}, {1, 4, 1, 4}, 1);
// Sorted ascending: dim1(1), dim3(1), dim0(4,singleton), dim2(4,singleton)
// Rotate axis=1 to front: [1, 3, 0, 2]
// strideOrder: [2, 0, 3, 1] → dim1 gets stride 1
EXPECT_EQ(strides[1], 1); // axis dim is most packed
}
TEST(TestShapeUtils, GenerateStridesWithPackedAxis3D)
{
// 3D tensor, axis=0
auto strides = generateStridesWithPackedAxis({12, 4, 1}, {3, 3, 4}, {3, 3, 4}, 0);
// Sorted ascending: [2,1,0], rotate axis=0 to front: [0,2,1]
// strideOrder: [0,2,1] → dim0 gets stride 1
EXPECT_EQ(strides[0], 1);
EXPECT_EQ(strides, (std::vector<int64_t>{1, 12, 3}));
}
TEST(TestShapeUtils, GenerateStridesWithPackedAxisLastAxis)
{
// Packing the last axis (already most packed in NCHW) preserves original layout
auto strides
= generateStridesWithPackedAxis({65536, 1024, 32, 1}, {2, 64, 32, 32}, {2, 64, 32, 32}, 3);
// Sorted ascending: [3,2,1,0], rotate axis=3 (at position 0) to front → no change
EXPECT_EQ(strides, (std::vector<int64_t>{65536, 1024, 32, 1}));
}
TEST(TestShapeUtils, IsTensorPackedTrueForPackedTensor)
{
const std::vector<int64_t> shape3D = {2, 3, 4};
auto strides3D = generateStrides(shape3D);
EXPECT_TRUE(isTensorPacked(shape3D, strides3D));
const std::vector<int64_t> shape4D = {2, 3, 4, 5};
auto stridesNchw = generateStrides(shape4D);
EXPECT_TRUE(isTensorPacked(shape4D, stridesNchw));
auto stridesNhwc = generateStrides(shape4D, {3, 0, 2, 1});
EXPECT_TRUE(isTensorPacked(shape4D, stridesNhwc));
const std::vector<int64_t> shape5D = {2, 3, 4, 5, 6};
auto stridesNcdhw = generateStrides(shape5D);
EXPECT_TRUE(isTensorPacked(shape5D, stridesNcdhw));
auto stridesNdhwc = generateStrides(shape5D, {4, 0, 3, 2, 1});
EXPECT_TRUE(isTensorPacked(shape5D, stridesNdhwc));
}
TEST(TestShapeUtils, IsTensorPackedFalseForNonPackedTensor)
{
const std::vector<int64_t> shape3D = {2, 3, 4};
const std::vector<int64_t> nonPackedStrides3D = {13, 4, 1};
EXPECT_FALSE(isTensorPacked(shape3D, nonPackedStrides3D));
const std::vector<int64_t> shape4D = {2, 3, 4, 5};
const std::vector<int64_t> nonPackedStrides4D = {50, 20, 5, 1};
EXPECT_FALSE(isTensorPacked(shape4D, nonPackedStrides4D));
const std::vector<int64_t> shape5D = {2, 3, 4, 5, 6};
const std::vector<int64_t> nonPackedStrides5D = {400, 120, 30, 6, 1};
EXPECT_FALSE(isTensorPacked(shape5D, nonPackedStrides5D));
}
TEST(TestShapeUtils, IsTensorPackedEmptyTensor)
{
// Empty tensors are considered packed
const std::vector<int64_t> emptyDims = {};
const std::vector<int64_t> emptyStrides = {};
EXPECT_TRUE(isTensorPacked(emptyDims, emptyStrides));
}
TEST(TestShapeUtils, IsTensorPackedSingleDimension)
{
// Single dimension tensors are packed if stride is 1
const std::vector<int64_t> dims = {10};
const std::vector<int64_t> strides = {1};
EXPECT_TRUE(isTensorPacked(dims, strides));
}
TEST(TestShapeUtils, IsTensorPackedWithDimensionSizeOne)
{
// Dimensions with size 1 - stride is irrelevant for that dimension
const std::vector<int64_t> dims1 = {1, 3, 4};
const std::vector<int64_t> strides1 = {12, 4, 1}; // Standard packed
EXPECT_TRUE(isTensorPacked(dims1, strides1));
const std::vector<int64_t> dims2 = {1, 3, 4};
const std::vector<int64_t> strides2 = {100, 4, 1}; // Large stride for size-1 dim, still packed
EXPECT_TRUE(isTensorPacked(dims2, strides2));
const std::vector<int64_t> dims3 = {2, 1, 4};
const std::vector<int64_t> strides3 = {4, 100, 1}; // Large stride for size-1 dim, still packed
EXPECT_TRUE(isTensorPacked(dims3, strides3));
}
TEST(TestShapeUtils, IsTensorPackedRowMajorVsColumnMajor)
{
// Both row-major and column-major 2x3 matrices should be packed
const std::vector<int64_t> dims = {2, 3};
// Row-major: strides = [3, 1]
const std::vector<int64_t> rowMajorStrides = {3, 1};
EXPECT_TRUE(isTensorPacked(dims, rowMajorStrides));
// Column-major: strides = [1, 2]
const std::vector<int64_t> colMajorStrides = {1, 2};
EXPECT_TRUE(isTensorPacked(dims, colMajorStrides));
}
TEST(TestShapeUtils, IsTensorPackedWithNegativeStrides)
{
// Negative strides are valid for reversed tensor dimensions
// A 2x3 tensor with reversed first dimension: strides = [-3, 1]
// The math still works: count = 6, space = (2-1)*(-3) + (3-1)*1 = -3 + 2 = -1
// So count != space + 1 (6 != 0), which means it's not packed
const std::vector<int64_t> dims = {2, 3};
const std::vector<int64_t> negativeStrides = {-3, 1};
// With negative strides, the tensor is typically not packed in the standard sense
EXPECT_FALSE(isTensorPacked(dims, negativeStrides));
}
TEST(TestShapeUtils, IsTensorPackedMismatchedSizes)
{
// Mismatched dims and strides should throw
const std::vector<int64_t> dims = {2, 3, 4};
const std::vector<int64_t> strides = {12, 4}; // Only 2 strides for 3 dims
EXPECT_THROW(isTensorPacked(dims, strides), std::invalid_argument);
}
TEST(TestShapeUtils, IsTensorPackedOverlappingStrides)
{
// Strides that cause overlap (e.g., both dimensions have stride 1)
const std::vector<int64_t> dims = {2, 3};
const std::vector<int64_t> overlappingStrides = {1, 1};
// count = 6, space = (2-1)*1 + (3-1)*1 = 1 + 2 = 3, count != space+1 (6 != 4)
EXPECT_FALSE(isTensorPacked(dims, overlappingStrides));
}
TEST(TestShapeUtils, IsTensorPackedAllOnesShape)
{
// Shape with all 1s should be packed
const std::vector<int64_t> dims = {1, 1, 1, 1};
const std::vector<int64_t> strides = {1, 1, 1, 1};
EXPECT_TRUE(isTensorPacked(dims, strides));
}
TEST(TestShapeUtils, IsTensorPackedLargeTensor)
{
// Test with a larger tensor
const std::vector<int64_t> dims = {4, 16, 32, 64};
auto strides = generateStrides(dims);
EXPECT_TRUE(isTensorPacked(dims, strides));
}
TEST(TestShapeUtils, IsTensorPackedCustomPackedLayout)
{
// Custom packed layout (not standard NCHW/NHWC but still contiguous)
const std::vector<int64_t> dims = {2, 3, 4};
// Order: [2, 0, 1] means dimension 2 is innermost, then 0, then 1
const std::vector<int64_t> customOrder = {2, 0, 1};
auto customStrides = generateStrides(dims, customOrder);
EXPECT_TRUE(isTensorPacked(dims, customStrides));
}
TEST(TestShapeUtils, GetDerivedShape5DValid)
{
const std::vector<int64_t> shape = {2, 4, 8, 16, 32};
auto derivedShape = getDerivedShape(shape);
EXPECT_EQ(derivedShape, (std::vector<int64_t>{1, 4, 1, 1, 1}));
}
TEST(TestShapeUtils, GetDerivedShapeThrowsForSingleDimension)
{
const std::vector<int64_t> shape = {10};
EXPECT_THROW(getDerivedShape(shape), std::runtime_error);
}
TEST(TestShapeUtils, CalculateGroupCountReturnsOneForStandardConvolution)
{
const std::vector<int64_t> inputDims = {1, 16, 32, 32};
const std::vector<int64_t> weightDims = {32, 16, 3, 3};
auto groupCount = calculateGroupCount(inputDims, weightDims);
EXPECT_EQ(groupCount, 1);
}
TEST(TestShapeUtils, CalculateGroupCountReturnsCorrectValueForGroupedConvolution)
{
const std::vector<int64_t> inputDims = {1, 16, 32, 32};
const std::vector<int64_t> weightDims = {32, 8, 3, 3};
auto groupCount = calculateGroupCount(inputDims, weightDims);
EXPECT_EQ(groupCount, 2);
}
TEST(TestShapeUtils, CalculateGroupCountThrowsForZeroWeightChannels)
{
const std::vector<int64_t> inputDims = {1, 16, 32, 32};
const std::vector<int64_t> weightDims = {32, 0, 3, 3};
EXPECT_THROW(calculateGroupCount(inputDims, weightDims), std::invalid_argument);
}
TEST(TestShapeUtils, CalculateGroupCountThrowsForZeroInputChannels)
{
const std::vector<int64_t> inputDims = {1, 0, 32, 32};
const std::vector<int64_t> weightDims = {32, 8, 3, 3};
EXPECT_THROW(calculateGroupCount(inputDims, weightDims), std::invalid_argument);
}
TEST(TestShapeUtils, CalculateGroupCountThrowsForInputDimsLessThanTwo)
{
const std::vector<int64_t> inputDims = {16};
const std::vector<int64_t> weightDims = {32, 16, 3, 3};
EXPECT_THROW(calculateGroupCount(inputDims, weightDims), std::invalid_argument);
}
TEST(TestShapeUtils, CalculateGroupCountThrowsForWeightDimsLessThanTwo)
{
const std::vector<int64_t> inputDims = {1, 16, 32, 32};
const std::vector<int64_t> weightDims = {32};
EXPECT_THROW(calculateGroupCount(inputDims, weightDims), std::invalid_argument);
}
TEST(TestShapeUtils, CalculateGroupCountThrowsForNonDivisibleChannels)
{
const std::vector<int64_t> inputDims = {1, 16, 32, 32};
const std::vector<int64_t> weightDims = {32, 7, 3, 3};
EXPECT_THROW(calculateGroupCount(inputDims, weightDims), std::invalid_argument);
}
TEST(TestShapeUtils, IsLayoutAgnosticTrueForAllOnes)
{
// Degenerate tensors (all dims=1) are layout-agnostic
EXPECT_TRUE(isLayoutAgnostic({1, 1, 1, 1}));
EXPECT_TRUE(isLayoutAgnostic({1, 1, 1, 1, 1}));
EXPECT_TRUE(isLayoutAgnostic({1}));
}
TEST(TestShapeUtils, IsLayoutAgnosticTrueForSingleNonTrivialDim)
{
// Channel-only/derived tensors (one non-trivial dimension)
EXPECT_TRUE(isLayoutAgnostic({1, 64, 1, 1}));
EXPECT_TRUE(isLayoutAgnostic({1, 128, 1, 1, 1}));
EXPECT_TRUE(isLayoutAgnostic({1, 1, 1, 32}));
EXPECT_TRUE(isLayoutAgnostic({10, 1, 1, 1}));
}
TEST(TestShapeUtils, IsLayoutAgnosticTrueForEmpty)
{
EXPECT_TRUE(isLayoutAgnostic({}));
}
TEST(TestShapeUtils, IsLayoutAgnosticFalseForMultipleNonTrivialDims)
{
// Standard input tensors with multiple non-trivial dimensions
EXPECT_FALSE(isLayoutAgnostic({2, 3, 4, 5}));
EXPECT_FALSE(isLayoutAgnostic({1, 64, 32, 32}));
EXPECT_FALSE(isLayoutAgnostic({2, 128, 1, 1}));
EXPECT_FALSE(isLayoutAgnostic({1, 1, 2, 3}));
}
TEST(TestShapeUtils, IsLayoutAgnosticBoundaryCase)
{
// Exactly two non-trivial dims should be false
EXPECT_FALSE(isLayoutAgnostic({2, 2, 1, 1}));
EXPECT_FALSE(isLayoutAgnostic({1, 2, 2, 1}));
}