Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
260 changes: 235 additions & 25 deletions .github/workflows/build-test-package.yml

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions .github/workflows/test-gpu.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
name: Test GPU

on: [pull_request]
on:
workflow_dispatch:
pull_request:
types: [opened, synchronize, reopened, labeled]

env:
itk-git-tag: "abf5fa10522a36bc51f42f20f426a622f42ed90d"

jobs:
build-test-gpu:
if: github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'gpu-ci')
runs-on: [self-hosted, gpu]
strategy:
matrix:
include:
- opencl-icd-loader-git-tag: "v2021.04.29"
opencl-headers-git-tag: "v2021.04.29"
opencl-version: "120"
opencl-version: "300"
vkfft-backend: 3
cmake-build-type: "MinSizeRel"
platform-name: "ubuntu-nvidia-gpu"
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/test-notebooks.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
name: Notebook tests

on: [push, pull_request]
on:
workflow_dispatch:
pull_request:
types: [opened, synchronize, reopened, labeled]

jobs:
nbmake:
if: github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'gpu-ci')
runs-on: [self-hosted, notebook-gpu]
name: Test notebooks with nbmake
strategy:
Expand Down
8 changes: 6 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ if(VKFFT_BACKEND EQUAL 1)
find_package(CUDAToolkit REQUIRED)
list(APPEND VkFFTBackend_SYSTEM_INCLUDE_DIRS ${CUDAToolkit_INCLUDE_DIRS})
elseif(VKFFT_BACKEND EQUAL 3)
add_compile_definitions(CL_TARGET_OPENCL_VERSION=120)
set(CL_TARGET_OPENCL_VERSION 300 CACHE STRING "OpenCL API version to target (e.g. 120, 300)")
add_compile_definitions(CL_TARGET_OPENCL_VERSION=${CL_TARGET_OPENCL_VERSION})

## When this module is loaded by an app, load OpenCL too.
set(VkFFTBackend_EXPORT_CODE_INSTALL "
Expand Down Expand Up @@ -53,7 +54,10 @@ elseif(VKFFT_BACKEND EQUAL 4)
if(NOT LevelZero_INCLUDE_DIR OR NOT LevelZero_LIBRARY)
message(FATAL_ERROR "VKFFT_BACKEND=4 (Level Zero) requires the oneAPI Level Zero loader (ze_loader) and headers (level_zero/ze_api.h). Install the 'level-zero' package or set LEVEL_ZERO_ROOT.")
endif()
list(APPEND VkFFTBackend_SYSTEM_INCLUDE_DIRS ${LevelZero_INCLUDE_DIR})
# VkFFT includes <ze_api.h> bare; this module uses <level_zero/ze_api.h> — both dirs needed.
list(APPEND VkFFTBackend_SYSTEM_INCLUDE_DIRS
${LevelZero_INCLUDE_DIR}
${LevelZero_INCLUDE_DIR}/level_zero)
list(APPEND VkFFTBackend_SYSTEM_LIBRARIES ${LevelZero_LIBRARY})

set(VkFFTBackend_EXPORT_CODE_INSTALL "
Expand Down
8 changes: 8 additions & 0 deletions include/itkVkDefinitions.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,12 @@
#define LEVEL_ZERO 4
#define METAL 5

// Defensive default: when VKFFT_BACKEND is not set on the command line
// (e.g. castxml wrapping invocations that bypass our top-level
// add_compile_definitions), fall back to OpenCL so vkFFT.h does not
// take the Vulkan branch and try to #include <vulkan/vulkan.h>.
#ifndef VKFFT_BACKEND
# define VKFFT_BACKEND OPENCL
#endif

#endif // itkVkDefinitions_h
10 changes: 8 additions & 2 deletions itk-module-init.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ if(${VKFFT_BACKEND} EQUAL 1)
elseif(${VKFFT_BACKEND} EQUAL 3)
find_package(OpenCL REQUIRED)
elseif(${VKFFT_BACKEND} EQUAL 4)
find_path(LevelZero_INCLUDE_DIR NAMES level_zero/ze_api.h)
find_library(LevelZero_LIBRARY NAMES ze_loader)
find_path(LevelZero_INCLUDE_DIR
NAMES level_zero/ze_api.h
HINTS ENV LEVEL_ZERO_ROOT ENV CMPLR_ROOT
PATH_SUFFIXES include)
find_library(LevelZero_LIBRARY
NAMES ze_loader
HINTS ENV LEVEL_ZERO_ROOT ENV CMPLR_ROOT
PATH_SUFFIXES lib lib64 lib/x64)
if(NOT LevelZero_INCLUDE_DIR OR NOT LevelZero_LIBRARY)
message(FATAL_ERROR "VKFFT_BACKEND=4 (Level Zero) requires the oneAPI Level Zero loader (ze_loader) and headers (level_zero/ze_api.h).")
endif()
Expand Down
16 changes: 15 additions & 1 deletion src/itkVkCommon.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,22 @@ VkCommon::ConfigureBackend()
uint64_t k{ 0 };
for (uint64_t j{ 0 }; j < numPlatforms; j++)
{
cl_uint numDevices;
// First probe: how many devices does this platform expose? An OpenCL
// platform with zero compute devices is legitimate (e.g. Apple's
// deprecated OpenCL framework on macOS 15 returns CL_DEVICE_NOT_FOUND
// for CL_DEVICE_TYPE_ALL). Skip such platforms; calling clGetDeviceIDs
// again with num_entries=0 would return CL_INVALID_VALUE.
cl_uint numDevices{ 0 };
resCL = clGetDeviceIDs(platforms[j], CL_DEVICE_TYPE_ALL, 0, nullptr, &numDevices);
if (resCL == CL_DEVICE_NOT_FOUND || numDevices == 0)
{
continue;
}
if (resCL != CL_SUCCESS)
{
std::cerr << __FILE__ "(" << __LINE__ << "): clGetDeviceIDs(count) returned " << resCL << std::endl;
return VkFFTResult{ VKFFT_ERROR_FAILED_TO_GET_DEVICE };
}
std::unique_ptr<cl_device_id[]> deviceListArray{ std::make_unique<cl_device_id[]>(numDevices) };
cl_device_id * deviceList{ &deviceListArray[0] };
if (!deviceList)
Expand Down
29 changes: 28 additions & 1 deletion test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,17 @@ itk_add_test(NAME itkVkForwardInverseFFTImageFilterTestDouble
)
_vkfft_disable_on_unsupported_fp64(itkVkForwardInverseFFTImageFilterTestDouble)

# pocl (CPU OpenCL) computes VkFFT's size-19 Bluestein inverse incorrectly, so
# the hosted pocl-backed leg runs the round-trip tests capped at size 16
# (radix-2/3/5/7 plus Bluestein primes 11/13). Real GPUs run the full sweep above.
itk_add_test(NAME itkVkForwardInverseFFTImageFilterTestFloatPoclSafe
COMMAND VkFFTBackendTestDriver itkVkForwardInverseFFTImageFilterTest float 16
)
itk_add_test(NAME itkVkForwardInverseFFTImageFilterTestDoublePoclSafe
COMMAND VkFFTBackendTestDriver itkVkForwardInverseFFTImageFilterTest double 16
)
_vkfft_disable_on_unsupported_fp64(itkVkForwardInverseFFTImageFilterTestDoublePoclSafe)

# -----------------------------------------------------------------------------
# ForwardInverse1DFFTImageFilterTest
# -----------------------------------------------------------------------------
Expand All @@ -181,6 +192,14 @@ itk_add_test(NAME itkVkForwardInverse1DFFTImageFilterTestDouble
)
_vkfft_disable_on_unsupported_fp64(itkVkForwardInverse1DFFTImageFilterTestDouble)

itk_add_test(NAME itkVkForwardInverse1DFFTImageFilterTestFloatPoclSafe
COMMAND VkFFTBackendTestDriver itkVkForwardInverse1DFFTImageFilterTest float 16
)
itk_add_test(NAME itkVkForwardInverse1DFFTImageFilterTestDoublePoclSafe
COMMAND VkFFTBackendTestDriver itkVkForwardInverse1DFFTImageFilterTest double 16
)
_vkfft_disable_on_unsupported_fp64(itkVkForwardInverse1DFFTImageFilterTestDoublePoclSafe)

# -----------------------------------------------------------------------------
# HalfHermitianFFTImageFilterTest
# -----------------------------------------------------------------------------
Expand All @@ -192,6 +211,14 @@ itk_add_test(NAME itkVkHalfHermitianFFTImageFilterTestDouble
)
_vkfft_disable_on_unsupported_fp64(itkVkHalfHermitianFFTImageFilterTestDouble)

itk_add_test(NAME itkVkHalfHermitianFFTImageFilterTestFloatPoclSafe
COMMAND VkFFTBackendTestDriver itkVkHalfHermitianFFTImageFilterTest float 16
)
itk_add_test(NAME itkVkHalfHermitianFFTImageFilterTestDoublePoclSafe
COMMAND VkFFTBackendTestDriver itkVkHalfHermitianFFTImageFilterTest double 16
)
_vkfft_disable_on_unsupported_fp64(itkVkHalfHermitianFFTImageFilterTestDoublePoclSafe)

# -----------------------------------------------------------------------------
# FFTImageFilterFactoryTest (instantiation only — runs on all platforms)
# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -356,7 +383,7 @@ if(VKFFT_BACKEND EQUAL 4 AND NOT VkFFTBackend_LEVEL_ZERO_RUNTIME_AVAILABLE)
itkVkDiscreteGaussianImageFilterTest
itkVkDiscreteGaussianImageFilterTest2
)
foreach(_suffix Float Double)
foreach(_suffix Float Double FloatPoclSafe DoublePoclSafe)
if(TEST ${_stem}${_suffix})
set_tests_properties(${_stem}${_suffix} PROPERTIES DISABLED TRUE)
endif()
Expand Down
11 changes: 6 additions & 5 deletions test/itkVkForwardInverse1DFFTImageFilterTest.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class ShowProgress : public itk::Command

template <typename PrecisionType>
int
runVkForwardInverse1DFFTImageFilterTest()
runVkForwardInverse1DFFTImageFilterTest(unsigned int maxSize = 20)
{
int testNumber{ 0 };
bool testsPassed{ true };
Expand All @@ -72,7 +72,7 @@ runVkForwardInverse1DFFTImageFilterTest()
bool firstPass{ true };

// Skip trivial case where 1D image of size 1 fails.
for (unsigned int mySize{ 2 }; mySize <= 20; ++mySize)
for (unsigned int mySize{ 2 }; mySize <= maxSize; ++mySize)
{
// We expect that anything evenly divisible by a prime number greater than 13
// will succeed with Bluestein's Algorithm implementation in VkFFT, though
Expand Down Expand Up @@ -184,14 +184,15 @@ runVkForwardInverse1DFFTImageFilterTest()
int
itkVkForwardInverse1DFFTImageFilterTest(int argc, char * argv[])
{
const std::string precision{ (argc > 1) ? argv[1] : "float" };
const std::string precision{ (argc > 1) ? argv[1] : "float" };
const unsigned int maxSize{ (argc > 2) ? static_cast<unsigned int>(std::stoul(argv[2])) : 20 };
if (precision == "double")
{
return runVkForwardInverse1DFFTImageFilterTest<double>();
return runVkForwardInverse1DFFTImageFilterTest<double>(maxSize);
}
if (precision == "float")
{
return runVkForwardInverse1DFFTImageFilterTest<float>();
return runVkForwardInverse1DFFTImageFilterTest<float>(maxSize);
}
std::cerr << "Unknown precision '" << precision << "'. Expected 'float' or 'double'." << std::endl;
return EXIT_FAILURE;
Expand Down
11 changes: 6 additions & 5 deletions test/itkVkForwardInverseFFTImageFilterTest.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class ShowProgress : public itk::Command

template <typename PrecisionType>
int
runVkForwardInverseFFTImageFilterTest()
runVkForwardInverseFFTImageFilterTest(unsigned int maxSize = 20)
{
int testNumber{ 0 };
bool testsPassed{ true };
Expand All @@ -72,7 +72,7 @@ runVkForwardInverseFFTImageFilterTest()
bool firstPass{ true };

// Skip trivial case where 1D image of size 1 fails.
for (unsigned int mySize{ 2 }; mySize <= 20; ++mySize, firstPass = false)
for (unsigned int mySize{ 2 }; mySize <= maxSize; ++mySize, firstPass = false)
{
// We expect that anything evenly divisible by a prime number greater than 13
// will succeed with Bluestein's Algorithm implementation in VkFFT, though
Expand Down Expand Up @@ -185,14 +185,15 @@ runVkForwardInverseFFTImageFilterTest()
int
itkVkForwardInverseFFTImageFilterTest(int argc, char * argv[])
{
const std::string precision{ (argc > 1) ? argv[1] : "float" };
const std::string precision{ (argc > 1) ? argv[1] : "float" };
const unsigned int maxSize{ (argc > 2) ? static_cast<unsigned int>(std::stoul(argv[2])) : 20 };
if (precision == "double")
{
return runVkForwardInverseFFTImageFilterTest<double>();
return runVkForwardInverseFFTImageFilterTest<double>(maxSize);
}
if (precision == "float")
{
return runVkForwardInverseFFTImageFilterTest<float>();
return runVkForwardInverseFFTImageFilterTest<float>(maxSize);
}
std::cerr << "Unknown precision '" << precision << "'. Expected 'float' or 'double'." << std::endl;
return EXIT_FAILURE;
Expand Down
11 changes: 6 additions & 5 deletions test/itkVkHalfHermitianFFTImageFilterTest.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class ShowProgress : public itk::Command

template <typename PrecisionType>
int
runVkHalfHermitianFFTImageFilterTest()
runVkHalfHermitianFFTImageFilterTest(unsigned int maxSize = 20)
{
int testNumber{ 0 };
bool testsPassed{ true };
Expand All @@ -71,7 +71,7 @@ runVkHalfHermitianFFTImageFilterTest()
typename RealImageType::IndexType index;
bool firstPass{ true };
// Skip trivial case where 1D image of size 1 fails.
for (unsigned int mySize{ 2 }; mySize <= 20; ++mySize, firstPass = false)
for (unsigned int mySize{ 2 }; mySize <= maxSize; ++mySize, firstPass = false)
{
// We expect that anything evenly divisible by a prime number greater than 13
// will succeed with Bluestein's Algorithm implementation in VkFFT, though
Expand Down Expand Up @@ -189,14 +189,15 @@ runVkHalfHermitianFFTImageFilterTest()
int
itkVkHalfHermitianFFTImageFilterTest(int argc, char * argv[])
{
const std::string precision{ (argc > 1) ? argv[1] : "float" };
const std::string precision{ (argc > 1) ? argv[1] : "float" };
const unsigned int maxSize{ (argc > 2) ? static_cast<unsigned int>(std::stoul(argv[2])) : 20 };
if (precision == "double")
{
return runVkHalfHermitianFFTImageFilterTest<double>();
return runVkHalfHermitianFFTImageFilterTest<double>(maxSize);
}
if (precision == "float")
{
return runVkHalfHermitianFFTImageFilterTest<float>();
return runVkHalfHermitianFFTImageFilterTest<float>(maxSize);
}
std::cerr << "Unknown precision '" << precision << "'. Expected 'float' or 'double'." << std::endl;
return EXIT_FAILURE;
Expand Down
25 changes: 20 additions & 5 deletions wrapping/dockcross-manylinux-build-module-wheels-opencl.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@
# Generate dockcross scripts

MANYLINUX_VERSION=${MANYLINUX_VERSION:=_2_28}
IMAGE_TAG=${IMAGE_TAG:=20221108-102ebcc}
OPENCL_ICD_LOADER_TAG=v2021.04.29
OPENCL_HEADERS_TAG=v2021.04.29
# Match ITKPythonPackage v6.0b02's dockcross image, which ships a recent
# enough `python -m build` to accept --verbose (the older 20221108 image
# fails with "unrecognized arguments: --verbose").
IMAGE_TAG=${IMAGE_TAG:=20260203-3dfb3ff}
OPENCL_ICD_LOADER_TAG=${OPENCL_ICD_LOADER_TAG:=v2025.07.22}
OPENCL_HEADERS_TAG=${OPENCL_HEADERS_TAG:=v2025.07.22}

docker run --rm dockcross/manylinux${MANYLINUX_VERSION}-x64:${IMAGE_TAG} > /tmp/dockcross-manylinux-x64
chmod u+x /tmp/dockcross-manylinux-x64
Expand Down Expand Up @@ -49,10 +52,22 @@ if ! test -d ./OpenCL-ICD-Loader; then
fi

# Build wheels
#
# Resolve the actual versioned libOpenCL.so filename produced by the
# OpenCL-ICD-Loader build. v2021.04.29 produced libOpenCL.so.1.2; the
# v2025.07.22 build produces libOpenCL.so.1.0.0. Bind-mount whichever the
# loader actually wrote.
OPENCL_SO=$(ls $(pwd)/OpenCL-ICD-Loader-build/libOpenCL.so.1.* 2>/dev/null | head -1)
if [[ -z "${OPENCL_SO}" ]]; then
echo "ERROR: could not find OpenCL-ICD-Loader-build/libOpenCL.so.1.* — did the loader build?" >&2
exit 1
fi
echo "Using OpenCL loader: ${OPENCL_SO}"

DOCKER_ARGS="-v $(pwd)/dist:/work/dist/ -v $script_dir/../ITKPythonPackage:/ITKPythonPackage -v $(pwd)/tools:/tools"
DOCKER_ARGS+=" -v $(pwd)/OpenCL-ICD-Loader/inc/CL:/usr/include/CL"
DOCKER_ARGS+=" -v $(pwd)/OpenCL-ICD-Loader-build/libOpenCL.so.1.2:/usr/lib64/libOpenCL.so.1"
DOCKER_ARGS+=" -v $(pwd)/OpenCL-ICD-Loader-build/libOpenCL.so.1.2:/usr/lib64/libOpenCL.so"
DOCKER_ARGS+=" -v ${OPENCL_SO}:/usr/lib64/libOpenCL.so.1"
DOCKER_ARGS+=" -v ${OPENCL_SO}:/usr/lib64/libOpenCL.so"
DOCKER_ARGS+=" -e MANYLINUX_VERSION"

/tmp/dockcross-manylinux-x64 \
Expand Down
Loading