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
13 changes: 13 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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_.*'

4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
65 changes: 65 additions & 0 deletions OPENCL_MACOS_26_ISSUE.md
Original file line number Diff line number Diff line change
@@ -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.
15 changes: 15 additions & 0 deletions libhmsbeagle/GPU/GPUInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -92,6 +103,10 @@ class GPUInterface {

int Initialize();

#ifdef FW_OPENCL
int Initialize(const OpenCLDiscoveryApi& discoveryApi);
#endif

int GetDeviceCount();

void SetDevice(int deviceNumber,
Expand Down
29 changes: 24 additions & 5 deletions libhmsbeagle/GPU/GPUInterfaceOpenCL.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<numPlatforms; i++) {
cl_uint numDevices = 0;
SAFE_CL(clGetDeviceIDs(platforms[i], CL_DEVICE_TYPE_ALL, 0, NULL, &numDevices));
status = discoveryApi.getDeviceIds(platforms[i], CL_DEVICE_TYPE_ALL, 0, NULL,
&numDevices);
if (status != CL_SUCCESS || numDevices == 0)
continue;
cl_device_id* deviceIds = new cl_device_id[numDevices];
SAFE_CL(clGetDeviceIDs(platforms[i], CL_DEVICE_TYPE_ALL, numDevices, deviceIds, NULL));
status = discoveryApi.getDeviceIds(platforms[i], CL_DEVICE_TYPE_ALL, numDevices,
deviceIds, NULL);
if (status != CL_SUCCESS) {
delete[] deviceIds;
continue;
}
for (int j=0; j<numDevices; j++) {
openClDeviceId = deviceIds[j];
BeagleDeviceImplementationCodes deviceCode = GetDeviceImplementationCode(-1);
Expand Down Expand Up @@ -1326,4 +1346,3 @@ void GPUInterface::ReleaseDevice(int deviceNumber) {


}; // namespace

24 changes: 24 additions & 0 deletions tests/opencl/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
add_executable(test_opencl_discovery
test_opencl_discovery.cpp)
target_compile_definitions(test_opencl_discovery PRIVATE FW_OPENCL)
target_include_directories(test_opencl_discovery PRIVATE ${OpenCL_INCLUDE_DIRS})
target_link_libraries(test_opencl_discovery PRIVATE hmsbeagle-opencl ${OpenCL_LIBRARIES})
add_test(NAME opencl_discovery_handles_enumeration_error COMMAND test_opencl_discovery)

add_executable(test_opencl_cpu_instance
test_opencl_cpu_instance.cpp)
target_link_libraries(test_opencl_cpu_instance PRIVATE hmsbeagle hmsbeagle-cpu hmsbeagle-opencl)
add_test(NAME opencl_plugin_allows_cpu_instance COMMAND test_opencl_cpu_instance)

if(APPLE)
set(BEAGLE_PLUGIN_TEST_PATH
"DYLD_LIBRARY_PATH=$<TARGET_FILE_DIR:hmsbeagle>:$<TARGET_FILE_DIR:hmsbeagle-cpu>:$<TARGET_FILE_DIR:hmsbeagle-opencl>")
elseif(UNIX)
set(BEAGLE_PLUGIN_TEST_PATH
"LD_LIBRARY_PATH=$<TARGET_FILE_DIR:hmsbeagle>:$<TARGET_FILE_DIR:hmsbeagle-cpu>:$<TARGET_FILE_DIR:hmsbeagle-opencl>")
endif()

if(BEAGLE_PLUGIN_TEST_PATH)
set_tests_properties(opencl_plugin_allows_cpu_instance PROPERTIES
ENVIRONMENT "${BEAGLE_PLUGIN_TEST_PATH}")
endif()
28 changes: 28 additions & 0 deletions tests/opencl/test_opencl_cpu_instance.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#include "libhmsbeagle/beagle.h"

#include <cstdio>

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;
}
47 changes: 47 additions & 0 deletions tests/opencl/test_opencl_discovery.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#include "libhmsbeagle/GPU/GPUInterface.h"

#include <cstdio>

namespace {

cl_platform_id fakePlatform = reinterpret_cast<cl_platform_id>(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;
}
Loading