Add ComputeManager; add SmoHooks for getting ClDevices, buffers

We added a new centralized OpenCL Compute manager. This can later
be extended to support CUDA, SyCL, etc. SMO can be configured at
build time to choose which API it will use for compute.

Moreover, the ComputeMgr allows us to register buffers which are
available to all cl_contexts.
This commit is contained in:
2025-11-19 22:33:30 -04:00
parent a910909ad5
commit 27b43c6686
9 changed files with 665 additions and 45 deletions
+8
View File
@@ -34,6 +34,9 @@ add_library(smocore STATIC
# SenseApis
stimBuffApis/stimBuffApiManager.cpp
# ComputeManager
computeManager/computeManager.cpp
# MindManager
mindManager/mindManager.cpp
)
@@ -47,6 +50,7 @@ target_include_directories(smocore PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_BINARY_DIR}
${Boost_INCLUDE_DIRS}
${OPENCL_INCLUDE_DIRS}
)
# Link against pthread for CPU affinity functions
@@ -55,4 +59,8 @@ target_link_libraries(smocore PRIVATE
Threads::Threads
Boost::system
Boost::log
${OPENCL_LIBRARIES}
)
target_link_directories(smocore PRIVATE
${OPENCL_LIBRARY_DIRS}
)
+307
View File
@@ -0,0 +1,307 @@
#include <computeManager/computeManager.h>
#include <iostream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <algorithm>
namespace smo {
namespace compute {
// Helper function to parse OpenCL version string
static std::pair<int, int> parseOpenClVersion(const std::string& versionStr)
{
size_t spacePos = versionStr.find(' ');
if (spacePos == std::string::npos) { return {-1, -1}; }
std::string versionNum = versionStr.substr(spacePos + 1);
size_t dotPos = versionNum.find('.');
if (dotPos == std::string::npos) { return {-1, -1}; }
try {
int major = std::stoi(versionNum.substr(0, dotPos));
int minor = std::stoi(versionNum.substr(dotPos + 1));
return {major, minor};
} catch (const std::exception&) {
return {-1, -1};
}
}
// Helper function to validate OpenCL version
static bool validateOpenClVersion(
std::string_view versionStr, std::string_view versionType,
int minMajor, int minMinor)
{
auto [major, minor] = parseOpenClVersion(std::string(versionStr));
if (major == -1 && minor == -1)
{
std::cerr << __func__ << ": failed to parse OpenCL " << versionType
<< " version: " << versionStr << std::endl;
return false;
}
if (major < minMajor || (major == minMajor && minor < minMinor))
{
std::cerr << __func__ << ": OpenCL " << versionType << " version "
<< major << "." << minor << " found, but " << minMajor << "."
<< minMinor << " or higher is required" << std::endl;
return false;
}
std::cout << __func__ << ": OpenCL " << versionType << " version: "
<< versionStr << std::endl;
return true;
}
ComputeDevice::ComputeDevice(cl_platform_id platformId, cl_device_id deviceId)
: platform(platformId), device(deviceId),
context(nullptr), commandQueue(nullptr)
{
cl_int err;
// Create context for this device
context = clCreateContext(
nullptr, 1, &device,
nullptr, nullptr, &err);
if (err != CL_SUCCESS || !context)
{
throw std::runtime_error(
std::string(__func__) + ": failed to create context for device: " +
std::to_string(err));
}
// Create command queue
cl_command_queue_properties queueProps = 0;
commandQueue = clCreateCommandQueue(
context, device, queueProps, &err);
if (err != CL_SUCCESS || !commandQueue)
{
clReleaseContext(context);
context = nullptr;
throw std::runtime_error(
std::string(__func__) + ": failed to create command queue for "
"device: " + std::to_string(err));
}
}
void ComputeManager::initialize()
{
if (initialized) { return; }
cl_int err;
// Get number of platforms
cl_uint numPlatforms = 0;
err = clGetPlatformIDs(0, nullptr, &numPlatforms);
if (err != CL_SUCCESS)
{
throw std::runtime_error(
std::string(__func__) + ": failed to get OpenCL platforms: " +
std::to_string(err));
}
if (numPlatforms == 0)
{
throw std::runtime_error(
std::string(__func__) + ": no OpenCL platforms found");
}
// Get all platforms
std::vector<cl_platform_id> platforms(numPlatforms);
err = clGetPlatformIDs(numPlatforms, platforms.data(), nullptr);
if (err != CL_SUCCESS)
{
throw std::runtime_error(
std::string(__func__) + ": failed to enumerate OpenCL platforms: " +
std::to_string(err));
}
// Enumerate devices for each platform
for (cl_uint p = 0; p < numPlatforms; ++p)
{
cl_platform_id platform = platforms[p];
// Check platform version
char platformVersion[128];
err = clGetPlatformInfo(
platform, CL_PLATFORM_VERSION,
sizeof(platformVersion), platformVersion, nullptr);
if (err == CL_SUCCESS)
{
if (!validateOpenClVersion(platformVersion, "platform", 1, 2))
{
std::cout << __func__ << ": skipping platform " << p
<< " with incompatible OpenCL version "
<< std::string(platformVersion) << std::endl;
continue;
}
}
// Get number of devices
cl_uint numDevices = 0;
err = clGetDeviceIDs(
platform, CL_DEVICE_TYPE_ALL, 0, nullptr, &numDevices);
if (err != CL_SUCCESS || numDevices == 0)
{
std::cout << __func__ << ": skipping platform " << p
<< " with no devices" << std::endl;
continue;
}
// Get all devices
std::vector<cl_device_id> platformDevices(numDevices);
err = clGetDeviceIDs(
platform, CL_DEVICE_TYPE_ALL, numDevices,
platformDevices.data(), nullptr);
if (err != CL_SUCCESS)
{
throw std::runtime_error(
std::string(__func__) + ": failed to enumerate devices for "
"platform " + std::to_string(p) + ": " + std::to_string(err));
}
// Create ComputeDevice for each device
for (cl_uint d = 0; d < numDevices; ++d)
{
cl_device_id device = platformDevices[d];
// Check device version
char deviceVersion[128];
err = clGetDeviceInfo(
device, CL_DEVICE_VERSION,
sizeof(deviceVersion), deviceVersion, nullptr);
if (err == CL_SUCCESS)
{
if (!validateOpenClVersion(deviceVersion, "device", 1, 2))
{
std::cout << __func__ << ": skipping device " << d
<< " with incompatible OpenCL version "
<< std::string(deviceVersion) << std::endl;
continue;
}
}
// Create ComputeDevice (constructor creates context and queue)
try
{
auto deviceObj = std::make_shared<ComputeDevice>(
platform, device);
devices.push_back(deviceObj);
}
catch (const std::runtime_error& e)
{
// Re-throw with more context about which device/platform
throw std::runtime_error(
std::string(__func__) + ": failed to create ComputeDevice "
"for device " + std::to_string(d) + " on platform " +
std::to_string(p) + ": " + e.what());
}
}
}
if (devices.empty())
{
throw std::runtime_error(
std::string(__func__) + ": no compatible OpenCL devices found");
}
initialized = true;
std::cout << __func__ << ": Initialized with " << devices.size()
<< " compute device(s)" << std::endl;
}
void ComputeManager::finalize()
{
if (!initialized) { return; }
// Release all devices (their shared_ptrs will clean up contexts/queues)
devices.clear();
initialized = false;
std::cout << __func__ << ": Finalized" << std::endl;
}
ClBuffer::ClBuffer(void* hostPtr, size_t size, cl_mem_flags flags,
const std::vector<std::shared_ptr<ComputeDevice>>& devices)
: hostPtr(hostPtr), size(size), flags(flags)
{
associations.reserve(devices.size());
// Create a buffer for each device's context
for (const auto& device : devices)
{
if (!device->context) { continue; }
cl_int err;
cl_mem_flags bufferFlags = CL_MEM_USE_HOST_PTR | flags;
cl_mem buffer = clCreateBuffer(
device->context,
bufferFlags,
size, hostPtr,
&err);
if (err != CL_SUCCESS || !buffer)
{
// Release any buffers already created before throwing
for (auto& assoc : associations)
{
if (assoc.buffer) {
clReleaseMemObject(assoc.buffer);
}
}
throw std::runtime_error(
std::string(__func__) + ": failed to create buffer for "
"device: " + std::to_string(err));
}
associations.emplace_back(buffer, device);
}
}
std::shared_ptr<ClBuffer>
ComputeManager::createUseHostPtrBuffer(
void* hostPtr, size_t size, cl_mem_flags flags)
{
if (!initialized)
{
std::cerr << __func__ << ": ComputeManager not initialized"
<< std::endl;
throw std::runtime_error(
std::string(__func__) + ": ComputeManager not initialized");
}
return std::make_shared<ClBuffer>(hostPtr, size, flags, devices);
}
void ComputeManager::releaseUseHostPtrBuffer(std::shared_ptr<ClBuffer> buffer)
{
// No-op: ClBuffer's destructor handles cleanup automatically
// This function exists for API compatibility
(void)buffer;
}
std::shared_ptr<ComputeDevice> ComputeManager::getDevice()
{
if (!initialized || devices.empty()) {
return nullptr;
}
// Return first available device
// In the future, this will filter based on ComputeDeviceConstraints
return devices[0];
}
void ComputeManager::releaseDevice(std::shared_ptr<ComputeDevice> device)
{
// Placeholder for future refcounting implementation
// Devices are only removed in finalize()
(void)device;
}
} // namespace compute
} // namespace smo
@@ -0,0 +1,105 @@
#ifndef _COMPUTE_MANAGER_H
#define _COMPUTE_MANAGER_H
#include <memory>
#include <vector>
#include <user/compute.h>
namespace smo {
namespace compute {
/**
* @brief Centralized OpenCL platform and device management
*
* Enumerates all OpenCL platforms and devices, maintains contexts and command
* queues, and provides methods to create buffers and access devices.
*/
class ComputeManager
{
public:
static ComputeManager& getInstance()
{
static ComputeManager instance;
return instance;
}
/**
* @brief Initialize ComputeManager by enumerating platforms and devices
*
* Enumerates all OpenCL platforms, then all devices on each platform,
* creating contexts and command queues for each device.
* Idempotent - can be called multiple times safely.
*/
void initialize();
/**
* @brief Finalize ComputeManager, releasing all resources
*
* Releases all contexts, command queues, and clears device list.
* Safe to call even if not initialized.
*/
void finalize();
/**
* @brief Create USE_HOST_PTR buffers on all contexts
*
* Creates a buffer using CL_MEM_USE_HOST_PTR on each device's context.
*
* @param hostPtr Host pointer to use
* @param size Size of buffer in bytes
* @param flags Additional OpenCL memory flags
* @return Shared pointer to ClBuffer managing buffers on all devices
*/
std::shared_ptr<ClBuffer> createUseHostPtrBuffer(
void* hostPtr, size_t size, cl_mem_flags flags);
/**
* @brief Release USE_HOST_PTR buffers
*
* Releases all buffers. This is a no-op since ClBuffer's destructor
* handles cleanup automatically.
*
* @param buffer Shared pointer to ClBuffer to release
*/
void releaseUseHostPtrBuffer(std::shared_ptr<ClBuffer> buffer);
/**
* @brief Get a compute device
*
* Returns the first available device. Later will accept
* ComputeDeviceConstraints for filtering.
*
* @return Shared pointer to ComputeDevice, or nullptr if no devices available
*/
std::shared_ptr<ComputeDevice> getDevice();
/**
* @brief Release a compute device
*
* Placeholder for future refcounting implementation.
* Currently a no-op - devices are only removed in finalize().
*
* @param device Shared pointer to ComputeDevice to release
*/
void releaseDevice(std::shared_ptr<ComputeDevice> device);
private:
ComputeManager() : initialized(false) {}
~ComputeManager() {}
// Non-copyable, non-movable
ComputeManager(const ComputeManager&) = delete;
ComputeManager& operator=(const ComputeManager&) = delete;
ComputeManager(ComputeManager&&) = delete;
ComputeManager& operator=(ComputeManager&&) = delete;
bool initialized;
std::vector<std::shared_ptr<ComputeDevice>> devices;
};
} // namespace compute
} // namespace smo
#endif // _COMPUTE_MANAGER_H
+3
View File
@@ -2,6 +2,7 @@
#include <mindManager/mindManager.h>
#include <deviceManager/deviceManager.h>
#include <stimBuffApis/stimBuffApiManager.h>
#include <computeManager/computeManager.h>
#include <salmanoff.h>
@@ -11,6 +12,7 @@ void initializeSalmanoff(void)
{
std::cout << __func__ << ": Entered." << std::endl;
compute::ComputeManager::getInstance().initialize();
mind::MindManager::getInstance().initialize();
stim_buff::StimBuffApiManager::getInstance().initialize();
device::DeviceManager::getInstance().initialize();
@@ -25,6 +27,7 @@ void shutdownSalmanoff(void)
device::DeviceManager::getInstance().finalize();
stim_buff::StimBuffApiManager::getInstance().finalize();
mind::MindManager::getInstance().finalize();
compute::ComputeManager::getInstance().finalize();
}
} // namespace smo
+38 -1
View File
@@ -13,6 +13,7 @@
#include <mind.h>
#include <deviceManager/deviceManager.h>
#include <marionette/marionette.h>
#include <computeManager/computeManager.h>
namespace fs = std::filesystem;
@@ -85,6 +86,36 @@ static OptionParser& OptionParser_getOptions()
return OptionParser::getOptions();
}
/* Local static functions to wrap ComputeManager methods for SmoCallbacks */
static std::shared_ptr<smo::compute::ClBuffer>
ComputeManager_createUseHostPtrBuffer(
void* hostPtr, size_t size, cl_mem_flags flags
)
{
return smo::compute::ComputeManager::getInstance().createUseHostPtrBuffer(
hostPtr, size, flags);
}
static void ComputeManager_releaseUseHostPtrBuffer(
std::shared_ptr<smo::compute::ClBuffer> buffer
)
{
smo::compute::ComputeManager::getInstance().releaseUseHostPtrBuffer(
buffer);
}
static std::shared_ptr<smo::compute::ComputeDevice> ComputeManager_getDevice()
{
return smo::compute::ComputeManager::getInstance().getDevice();
}
static void ComputeManager_releaseDevice(
std::shared_ptr<smo::compute::ComputeDevice> device
)
{
smo::compute::ComputeManager::getInstance().releaseDevice(device);
}
/* Hooks to be provided to stimBuffApiLibs, enabling them to call into Salmanoff
* code.
*/
@@ -92,7 +123,13 @@ static SmoCallbacks smoCallbacks =
{
.searchForLibInSmoSearchPaths = searchForLibInSmoSearchPaths,
.ComponentThread_getSelf = ComponentThread_getSelf,
.OptionParser_getOptions = OptionParser_getOptions
.OptionParser_getOptions = OptionParser_getOptions,
.ComputeManager_createUseHostPtrBuffer =
ComputeManager_createUseHostPtrBuffer,
.ComputeManager_releaseUseHostPtrBuffer =
ComputeManager_releaseUseHostPtrBuffer,
.ComputeManager_getDevice = ComputeManager_getDevice,
.ComputeManager_releaseDevice = ComputeManager_releaseDevice
};
/* Static file-scope threading model object for senseApi libraries */