Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 226 additions & 0 deletions .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
name: Build

# Builds HPTT on Linux, macOS and Windows, on both x86-64 and ARM64, and
# then consumes it from a separate project through
# `find_package(hptt CONFIG)` -- once against the build tree and once
# against an install prefix. The consumer transposes random tensors and
# compares against a naive reference, so a green job means the exported
# package links *and* the library computes correct results on that
# platform.

on:
# No `branches:` filter: a pull request targeting any branch is built,
# which is what makes stacked PRs (one PR based on another's branch)
# get checked too.
pull_request:
push:
branches:
- main
workflow_dispatch:

concurrency:
# One group per PR, so pushing a new commit cancels the runs still
# queued or in flight for the previous one. Pushes to main are keyed by
# run_id instead and never cancel each other -- every merged commit
# keeps its own result.
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
build:
name: ${{ matrix.name }}
runs-on: ${{ matrix.runner }}
strategy:
# One platform failing should not hide the results of the others.
fail-fast: false
matrix:
include:
- name: linux-x64
runner: ubuntu-latest
- name: linux-arm64
runner: ubuntu-24.04-arm
Comment thread
IvanaGyro marked this conversation as resolved.
- name: macos-x64
runner: macos-15-intel
- name: macos-arm64
runner: macos-latest
- name: windows-x64
runner: windows-latest
msvc_arch: x64
- name: windows-arm64
runner: windows-11-arm
msvc_arch: arm64

defaults:
run:
# bash everywhere, including Windows, so one set of steps covers
# all six jobs. -e / -o pipefail restore the fail-fast behaviour
# that overriding `shell:` would otherwise drop.
shell: bash -eo pipefail {0}

env:
BUILD_DIR: ${{ github.workspace }}/build
INSTALL_DIR: ${{ github.workspace }}/install
CONSUMER_SRC_DIR: ${{ github.workspace }}/tests/downstream_find_package

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

# Re-running a PR job reuses the merge commit built when the run was
# first created, not a fresh merge with the current target branch.
# Merging here keeps the job honest about the branch as it stands now.
# A committer identity is required because a non-fast-forward merge
# creates a merge commit.
- name: Merge with latest target branch (pull_request only)
if: github.event_name == 'pull_request'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git fetch origin ${{ github.event.pull_request.base.ref }}
git merge --no-edit origin/${{ github.event.pull_request.base.ref }}

# AppleClang ships no OpenMP runtime, so find_package(OpenMP REQUIRED)
# fails on macOS without libomp. OpenMP_ROOT is passed at configure
# time below.
- name: Install libomp (macOS)
if: runner.os == 'macOS'
run: brew install libomp

# The runner images do not all ship Ninja, and the ones that do are
# not consistent about it across architectures.
- name: Install Ninja
run: |
if command -v ninja >/dev/null 2>&1; then
ninja --version
exit 0
fi
case "$RUNNER_OS" in
Linux) sudo apt-get update && sudo apt-get install -y ninja-build ;;
macOS) brew install ninja ;;
Windows) choco install ninja -y --no-progress ;;
esac
ninja --version

# Puts cl.exe, the Windows SDK headers and the import libraries on
# PATH/INCLUDE/LIB. The Ninja generator, unlike the Visual Studio
# one, does not locate the toolchain by itself. Ninja itself is
# preinstalled on the Windows runner images.
- name: Set up MSVC (Windows)
if: runner.os == 'Windows'
uses: ilammy/msvc-dev-cmd@v1
with:
arch: ${{ matrix.msvc_arch }}

# Installs ccache for the current OS and restores/saves its cache.
# The key is per matrix job so the six platforms never share objects.
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2
with:
key: ${{ matrix.name }}
max-size: 500M

- name: Configure HPTT
# ccache is wired in through CMake's compiler-launcher hook rather
# than by shadowing the compiler on PATH, which keeps working with
# the absolute compiler paths CMake records.
#
# CMP0141 + MSVC_DEBUG_INFORMATION_FORMAT=Embedded put debug info
# in the object files (/Z7). ccache cannot cache compilations that
# write to a shared .pdb, so without this every Windows compile is
# a cache miss.
run: |
EXTRA=()
if [ "$RUNNER_OS" = "macOS" ]; then
EXTRA+=("-DOpenMP_ROOT=$(brew --prefix libomp)")
fi
if [ "$RUNNER_OS" = "Windows" ]; then
EXTRA+=(-DCMAKE_C_COMPILER=cl -DCMAKE_CXX_COMPILER=cl)
EXTRA+=(-DCMAKE_POLICY_DEFAULT_CMP0141=NEW)
EXTRA+=(-DCMAKE_MSVC_DEBUG_INFORMATION_FORMAT=Embedded)
fi
cmake -S . -B "$BUILD_DIR" -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
"${EXTRA[@]}"

- name: Build HPTT
run: cmake --build "$BUILD_DIR"

# --- Downstream consumer: build tree ------------------------------
# export(TARGETS ...) plus the build-dir hpttConfig.cmake make the
# package resolvable before anything is installed; hptt_DIR points
# find_package straight at the build directory.
- name: Consumer against the BUILD TREE
run: |
EXTRA=()
[ "$RUNNER_OS" = "Windows" ] && EXTRA+=(-DCMAKE_CXX_COMPILER=cl)
cmake -S "$CONSUMER_SRC_DIR" -B "${{ github.workspace }}/consumer-build-tree" \
-G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-Dhptt_DIR="$BUILD_DIR" "${EXTRA[@]}"
Comment thread
IvanaGyro marked this conversation as resolved.
cmake --build "${{ github.workspace }}/consumer-build-tree"

- name: Run BUILD TREE consumer
run: |
cd "${{ github.workspace }}/consumer-build-tree"
./consumer
./consumer_dyn

# --- Downstream consumer: install tree ----------------------------
- name: Install HPTT
run: cmake --install "$BUILD_DIR"

- name: Consumer against the INSTALL TREE
# A fresh build directory resolving the package from the install
# prefix through CMAKE_PREFIX_PATH, the way a real downstream user
# would find an installed HPTT.
run: |
EXTRA=()
[ "$RUNNER_OS" = "Windows" ] && EXTRA+=(-DCMAKE_CXX_COMPILER=cl)
cmake -S "$CONSUMER_SRC_DIR" -B "${{ github.workspace }}/consumer-install-tree" \
-G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_PREFIX_PATH="$INSTALL_DIR" "${EXTRA[@]}"
cmake --build "${{ github.workspace }}/consumer-install-tree"

- name: Run INSTALL TREE consumer
run: |
cd "${{ github.workspace }}/consumer-install-tree"
./consumer
./consumer_dyn

# Guards the artifact-naming rule the Windows build depends on: the
# static library and the DLL's import library must never claim the
# same path. The Ninja generator only rejects the collision when both
# targets are enabled, which is the configuration built above.
- name: Check Windows artifact names are distinct
if: runner.os == 'Windows'
run: |
test -f "$INSTALL_DIR/lib/hptt.lib"
test -f "$INSTALL_DIR/lib/hptt_dyn.lib"
test -f "$INSTALL_DIR/bin/hptt.dll"

# HPTT_BUILD_SHARED=OFF has to keep configuring, building and
# exporting on its own -- it is how a parent project that links only
# the static archive embeds HPTT.
- name: Build with HPTT_BUILD_SHARED=OFF
run: |
EXTRA=()
if [ "$RUNNER_OS" = "macOS" ]; then
EXTRA+=("-DOpenMP_ROOT=$(brew --prefix libomp)")
fi
if [ "$RUNNER_OS" = "Windows" ]; then
EXTRA+=(-DCMAKE_C_COMPILER=cl -DCMAKE_CXX_COMPILER=cl)
EXTRA+=(-DCMAKE_POLICY_DEFAULT_CMP0141=NEW)
EXTRA+=(-DCMAKE_MSVC_DEBUG_INFORMATION_FORMAT=Embedded)
fi
cmake -S . -B "${{ github.workspace }}/build-static-only" -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DHPTT_BUILD_SHARED=OFF \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
"${EXTRA[@]}"
cmake --build "${{ github.workspace }}/build-static-only"
31 changes: 31 additions & 0 deletions tests/downstream_find_package/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Standalone consumer used by .github/workflows/ci-build.yml.
#
# It is deliberately *not* part of the top-level HPTT build: it resolves
# HPTT the way a real downstream project does, through
# `find_package(hptt CONFIG)` and the `hptt::` imported targets, so it
# exercises the generated hpttConfig.cmake / hpttTargets.cmake rather than
# the in-tree target names. The workflow configures it twice, once against
# the build tree (via hptt_DIR) and once against an install prefix (via
# CMAKE_PREFIX_PATH).
cmake_minimum_required(VERSION 3.21)
project(hptt_downstream_consumer LANGUAGES CXX)

find_package(hptt CONFIG REQUIRED)

add_executable(consumer consumer.cpp)
target_link_libraries(consumer PRIVATE hptt::hptt_static)

# hptt::hptt_dyn only exists when HPTT was built with HPTT_BUILD_SHARED=ON,
# so a static-only HPTT still configures here.
if(TARGET hptt::hptt_dyn)
add_executable(consumer_dyn consumer.cpp)
target_link_libraries(consumer_dyn PRIVATE hptt::hptt_dyn)

# On Windows the DLL has to sit next to the executable to be found at
# run time. TARGET_RUNTIME_DLLS is empty on platforms that use rpath,
# so the command is skipped there rather than guarded on WIN32.
add_custom_command(TARGET consumer_dyn POST_BUILD
COMMAND ${CMAKE_COMMAND} -E $<IF:$<BOOL:$<TARGET_RUNTIME_DLLS:consumer_dyn>>,copy,true>
$<TARGET_RUNTIME_DLLS:consumer_dyn> $<TARGET_FILE_DIR:consumer_dyn>
COMMAND_EXPAND_LISTS)
endif()
121 changes: 121 additions & 0 deletions tests/downstream_find_package/consumer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Correctness check for HPTT: transposes random tensors and compares against
// a naive index-arithmetic reference. Covers all four instantiated scalar
// types, several dimensionalities/permutations, and beta != 0.
#include <hptt.h>

#include <algorithm>
#include <complex>
#include <cstdio>
#include <random>
#include <vector>

namespace {

int failures = 0;

inline double mag(float v) { return std::abs(static_cast<double>(v)); }
inline double mag(double v) { return std::abs(v); }
inline double mag(std::complex<float> v) { return std::abs(std::complex<double>(v)); }
inline double mag(std::complex<double> v) { return std::abs(v); }

template <typename T> struct Sampler {
static T get(std::mt19937 &rng) {
std::uniform_real_distribution<double> d(-1.0, 1.0);
return static_cast<T>(d(rng));
}
};
template <typename R> struct Sampler<std::complex<R>> {
static std::complex<R> get(std::mt19937 &rng) {
std::uniform_real_distribution<double> d(-1.0, 1.0);
return std::complex<R>(static_cast<R>(d(rng)), static_cast<R>(d(rng)));
}
};

// B_{perm(i)} = alpha * A_i + beta * B_{perm(i)}, column-major on both sides.
template <typename T>
void reference(const std::vector<int> &sizeA, const std::vector<int> &perm, T alpha,
const std::vector<T> &A, T beta, std::vector<T> &B) {
const int dim = static_cast<int>(sizeA.size());
std::vector<int> sizeB(dim);
for (int k = 0; k < dim; ++k) sizeB[k] = sizeA[perm[k]];

std::vector<size_t> ldA(dim, 1), ldB(dim, 1);
for (int k = 1; k < dim; ++k) ldA[k] = ldA[k - 1] * static_cast<size_t>(sizeA[k - 1]);
for (int k = 1; k < dim; ++k) ldB[k] = ldB[k - 1] * static_cast<size_t>(sizeB[k - 1]);

std::vector<int> idx(dim, 0);
for (size_t n = 0; n < A.size(); ++n) {
size_t offA = 0, offB = 0;
for (int k = 0; k < dim; ++k) offA += static_cast<size_t>(idx[k]) * ldA[k];
for (int k = 0; k < dim; ++k) offB += static_cast<size_t>(idx[perm[k]]) * ldB[k];
B[offB] = alpha * A[offA] + beta * B[offB];
for (int k = 0; k < dim; ++k) {
if (++idx[k] < sizeA[k]) break;
idx[k] = 0;
}
}
}

template <typename T>
void run(const char *label, const std::vector<int> &sizeA, const std::vector<int> &perm,
T alpha, T beta, int numThreads, double limit) {
const int dim = static_cast<int>(sizeA.size());
size_t total = 1;
for (int s : sizeA) total *= static_cast<size_t>(s);

std::mt19937 rng(12345u + static_cast<unsigned>(dim) * 7u);
std::vector<T> A(total), B(total);
for (size_t i = 0; i < total; ++i) A[i] = Sampler<T>::get(rng);
for (size_t i = 0; i < total; ++i) B[i] = Sampler<T>::get(rng);
std::vector<T> Bref = B;

reference<T>(sizeA, perm, alpha, A, beta, Bref);

auto plan = hptt::create_plan(perm.data(), dim, alpha, A.data(), sizeA.data(), nullptr,
beta, B.data(), nullptr, hptt::ESTIMATE, numThreads);
plan->execute();

double worst = 0.0;
for (size_t i = 0; i < total; ++i) worst = std::max(worst, mag(B[i] - Bref[i]));

if (!(worst <= limit)) {
std::printf(" FAIL %-34s max|B-Bref| = %g\n", label, worst);
++failures;
} else {
std::printf(" ok %-34s max|B-Bref| = %g\n", label, worst);
}
}

} // namespace

int main() {
const double ftol = 1e-4, dtol = 1e-12;

std::printf("hptt correctness (numThreads=1):\n");
run<float>("float 2D {64,32} perm{1,0}", {64, 32}, {1, 0}, 1.0f, 0.0f, 1, ftol);
run<double>("double 2D {64,32} perm{1,0}", {64, 32}, {1, 0}, 1.0, 0.0, 1, dtol);
run<double>("double 3D {17,9,13} perm{2,0,1}", {17, 9, 13}, {2, 0, 1}, 1.0, 0.0, 1, dtol);
run<double>("double 3D beta!=0", {17, 9, 13}, {2, 0, 1}, 2.5, -1.25, 1, dtol);
run<double>("double 4D {8,7,6,5} perm{3,1,0,2}", {8, 7, 6, 5}, {3, 1, 0, 2}, 1.0, 0.0, 1, dtol);
run<double>("double 5D {5,4,3,6,7} perm{4,0,3,1,2}", {5, 4, 3, 6, 7}, {4, 0, 3, 1, 2}, 1.0, 0.0, 1, dtol);
run<double>("double identity perm {33,21}", {33, 21}, {0, 1}, 1.0, 0.0, 1, dtol);
run<std::complex<float>>("cfloat 3D perm{2,1,0}", {12, 10, 9}, {2, 1, 0},
std::complex<float>(1.0f, 0.0f), std::complex<float>(0.0f, 0.0f), 1, ftol);
run<std::complex<double>>("cdouble 3D alpha,beta complex", {12, 10, 9}, {2, 1, 0},
std::complex<double>(1.5, -0.5), std::complex<double>(0.25, 0.75), 1, dtol);

std::printf("hptt correctness (numThreads=4):\n");
run<double>("double 2D large {512,256}", {512, 256}, {1, 0}, 1.0, 0.0, 4, dtol);
run<double>("double 3D large {128,64,32}", {128, 64, 32}, {2, 0, 1}, 1.0, 0.0, 4, dtol);
run<double>("double 4D large beta!=0", {32, 24, 16, 12}, {3, 1, 0, 2}, 1.5, 0.5, 4, dtol);
run<float>("float 3D large {128,64,32}", {128, 64, 32}, {1, 2, 0}, 1.0f, 0.0f, 4, ftol);
run<std::complex<double>>("cdouble 3D large", {64, 32, 16}, {2, 0, 1},
std::complex<double>(1.0, 0.0), std::complex<double>(0.0, 0.0), 4, dtol);

if (failures) {
std::printf("\n%d FAILURE(S)\n", failures);
return 1;
}
std::printf("\nall checks passed\n");
return 0;
}
Loading