diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3b80aeaf..40743728 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -48,4 +48,17 @@ jobs: cmake .. make -j file libhmsbeagle/libhmsbeagle.so.* | grep aarch64 + + opencl-discovery-macos-arm64: + name: OpenCL discovery on macOS arm64 + runs-on: macos-14 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build OpenCL tests + run: | + cmake -S . -B build -DBUILD_OPENCL=ON -DBUILD_CUDA=OFF -DBUILD_JNI=OFF + cmake --build build --target test_opencl_discovery test_opencl_cpu_instance + - name: Run OpenCL discovery tests + run: ctest --test-dir build --output-on-failure -R 'opencl_.*' diff --git a/CMakeLists.txt b/CMakeLists.txt index ba294bde..bc4ed05c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -195,6 +195,10 @@ enable_testing() add_subdirectory(examples) +if(TARGET hmsbeagle-opencl) + add_subdirectory(tests/opencl) +endif() + IF(BEAGLE_DEBUG_KERNELS) add_subdirectory(tests/tensor_cores) add_subdirectory(tests/basta) diff --git a/OPENCL_MACOS_26_ISSUE.md b/OPENCL_MACOS_26_ISSUE.md new file mode 100644 index 00000000..39613010 --- /dev/null +++ b/OPENCL_MACOS_26_ISSUE.md @@ -0,0 +1,65 @@ +# OpenCL Plugin Terminates CPU-Only Clients On macOS 26 / Apple Silicon + +## Summary + +The optional OpenCL plugin terminates the entire process during plugin discovery +on an Apple M4 Max running macOS 26.5.2. This prevents CPU-only BEAGLE clients +from starting, even when their `beagleCreateInstance()` requirements explicitly +request `BEAGLE_FLAG_PROCESSOR_CPU` and `BEAGLE_FLAG_PRECISION_DOUBLE`. + +## Environment + +- BEAGLE source: `ecf2ba5d64e6e5cc56913a541ef5a75b3b643ec9` (4.1.0 build) +- macOS 26.5.2 (25F84), arm64 +- Apple M4 Max, 40-core integrated GPU +- Xcode 26.6 (17F113) +- OpenCL.framework supplied by macOS + +## Reproduction + +1. Configure and build BEAGLE with `BUILD_OPENCL=ON`. +2. Run a client that calls `beagleCreateInstance()` with CPU and double + precision as requirement flags. + +The call loads all available plugins before choosing a matching resource. The +OpenCL plugin invokes `GPUInterface::Initialize()`, which calls: + +```cpp +clGetDeviceIDs(platforms[i], CL_DEVICE_TYPE_ALL, 0, NULL, &numDevices) +``` + +On this system that call returns `CL_INVALID_VALUE`. The `SAFE_CL` macro in +`libhmsbeagle/GPU/GPUInterfaceOpenCL.cpp` then calls `exit(-1)`, so the client +cannot fall back to the CPU plugin. + +Observed output before this fix: + +```text +OpenCL error: CL_INVALID_VALUE from file <.../GPUInterfaceOpenCL.cpp>, line 115. +``` + +## Expected Behavior + +OpenCL discovery is optional. A platform that reports an enumeration error or +no usable devices should make the OpenCL plugin expose no resources, while CPU +and other plugins remain usable. It should not terminate the host process. + +## Proposed Fix + +Handle errors and empty results from `clGetPlatformIDs()` and +`clGetDeviceIDs()` locally in `GPUInterface::Initialize()` by returning zero +devices or skipping the affected platform. Keep `SAFE_CL` for operations after +a concrete OpenCL device has been selected. + +## Validation + +After applying the accompanying source patch, a BEAGLE build with both CPU and +OpenCL plugins present successfully ran Migrate's standalone four-pattern, +three-tip JC69 fixed-tree proof. The BEAGLE result matched the independently +computed likelihood: `-22.849157383712207`. + +The patch also adds a deterministic CTest that injects `CL_INVALID_VALUE` from +`clGetDeviceIDs()` and verifies that discovery returns zero resources without +exiting. A GitHub Actions `macos-14` arm64 job builds with OpenCL enabled and +runs both that test and a real CPU-required instance-creation test with the +OpenCL plugin present. diff --git a/libhmsbeagle/GPU/GPUInterface.h b/libhmsbeagle/GPU/GPUInterface.h index e02cc74d..a78173e7 100644 --- a/libhmsbeagle/GPU/GPUInterface.h +++ b/libhmsbeagle/GPU/GPUInterface.h @@ -65,6 +65,17 @@ #endif #endif +#ifdef FW_OPENCL +struct OpenCLDiscoveryApi { + typedef cl_int (*GetPlatformIds)(cl_uint, cl_platform_id*, cl_uint*); + typedef cl_int (*GetDeviceIds)(cl_platform_id, cl_device_type, cl_uint, + cl_device_id*, cl_uint*); + + GetPlatformIds getPlatformIds; + GetDeviceIds getDeviceIds; +}; +#endif + class GPUInterface { private: int numStreams; @@ -92,6 +103,10 @@ class GPUInterface { int Initialize(); +#ifdef FW_OPENCL + int Initialize(const OpenCLDiscoveryApi& discoveryApi); +#endif + int GetDeviceCount(); void SetDevice(int deviceNumber, diff --git a/libhmsbeagle/GPU/GPUInterfaceOpenCL.cpp b/libhmsbeagle/GPU/GPUInterfaceOpenCL.cpp index 546cf049..beef704c 100644 --- a/libhmsbeagle/GPU/GPUInterfaceOpenCL.cpp +++ b/libhmsbeagle/GPU/GPUInterfaceOpenCL.cpp @@ -100,21 +100,41 @@ GPUInterface::~GPUInterface() { } int GPUInterface::Initialize() { + const OpenCLDiscoveryApi discoveryApi = {clGetPlatformIDs, clGetDeviceIDs}; + return Initialize(discoveryApi); +} + +int GPUInterface::Initialize(const OpenCLDiscoveryApi& discoveryApi) { #ifdef BEAGLE_DEBUG_FLOW fprintf(stderr,"\t\t\tEntering GPUInterface::Initialize\n"); #endif cl_uint numPlatforms = 0; - SAFE_CL(clGetPlatformIDs(0, NULL, &numPlatforms)); + cl_int status = discoveryApi.getPlatformIds(0, NULL, &numPlatforms); + if (status != CL_SUCCESS || numPlatforms == 0) + return 0; + cl_platform_id* platforms = new cl_platform_id[numPlatforms]; - SAFE_CL(clGetPlatformIDs(numPlatforms, platforms, NULL)); + status = discoveryApi.getPlatformIds(numPlatforms, platforms, NULL); + if (status != CL_SUCCESS) { + delete[] platforms; + return 0; + } int deviceAdded = 0; for (int i=0; i:$:$") +elseif(UNIX) + set(BEAGLE_PLUGIN_TEST_PATH + "LD_LIBRARY_PATH=$:$:$") +endif() + +if(BEAGLE_PLUGIN_TEST_PATH) + set_tests_properties(opencl_plugin_allows_cpu_instance PROPERTIES + ENVIRONMENT "${BEAGLE_PLUGIN_TEST_PATH}") +endif() diff --git a/tests/opencl/test_opencl_cpu_instance.cpp b/tests/opencl/test_opencl_cpu_instance.cpp new file mode 100644 index 00000000..8be28848 --- /dev/null +++ b/tests/opencl/test_opencl_cpu_instance.cpp @@ -0,0 +1,28 @@ +#include "libhmsbeagle/beagle.h" + +#include + +int main() +{ + BeagleInstanceDetails details; + const int instance = beagleCreateInstance( + 3, 2, 3, 4, 4, 1, 4, 1, 0, NULL, 0, + BEAGLE_FLAG_PROCESSOR_CPU | BEAGLE_FLAG_PRECISION_DOUBLE | + BEAGLE_FLAG_THREADING_NONE, + BEAGLE_FLAG_PROCESSOR_CPU | BEAGLE_FLAG_PRECISION_DOUBLE, &details); + + if (instance < 0) { + std::fprintf(stderr, "CPU instance creation failed with BEAGLE status %d\n", instance); + return 1; + } + if ((details.flags & BEAGLE_FLAG_PROCESSOR_CPU) == 0) { + std::fprintf(stderr, "BEAGLE selected a non-CPU resource\n"); + beagleFinalizeInstance(instance); + return 1; + } + if (beagleFinalizeInstance(instance) != BEAGLE_SUCCESS) { + std::fprintf(stderr, "BEAGLE failed to finalize the CPU instance\n"); + return 1; + } + return 0; +} diff --git a/tests/opencl/test_opencl_discovery.cpp b/tests/opencl/test_opencl_discovery.cpp new file mode 100644 index 00000000..8d573728 --- /dev/null +++ b/tests/opencl/test_opencl_discovery.cpp @@ -0,0 +1,47 @@ +#include "libhmsbeagle/GPU/GPUInterface.h" + +#include + +namespace { + +cl_platform_id fakePlatform = reinterpret_cast(1); +int deviceQueryCount = 0; + +cl_int fakeGetPlatformIds(cl_uint numEntries, cl_platform_id* platforms, + cl_uint* numPlatforms) +{ + if (numPlatforms != NULL) + *numPlatforms = 1; + if (numEntries == 0) + return CL_SUCCESS; + if (numEntries == 1 && platforms != NULL) { + platforms[0] = fakePlatform; + return CL_SUCCESS; + } + return CL_INVALID_VALUE; +} + +cl_int fakeGetDeviceIds(cl_platform_id platform, cl_device_type, cl_uint, + cl_device_id*, cl_uint*) +{ + ++deviceQueryCount; + return platform == fakePlatform ? CL_INVALID_VALUE : CL_INVALID_PLATFORM; +} + +} // namespace + +int main() +{ + opencl_device::OpenCLDiscoveryApi discoveryApi = {fakeGetPlatformIds, fakeGetDeviceIds}; + opencl_device::GPUInterface gpu; + + if (gpu.Initialize(discoveryApi) != 0) { + std::fprintf(stderr, "OpenCL discovery reported devices after an enumeration error\n"); + return 1; + } + if (deviceQueryCount != 1) { + std::fprintf(stderr, "OpenCL discovery did not query exactly one fake platform\n"); + return 1; + } + return 0; +}