C++实现OpenCL/Vulkan计算着色器:底层API封装与多设备并发管理

好的,下面是一篇关于C++实现OpenCL/Vulkan计算着色器:底层API封装与多设备并发管理的技术文章,以讲座模式呈现。

C++ 实现 OpenCL/Vulkan 计算着色器:底层 API 封装与多设备并发管理

大家好!今天我们来聊聊如何使用 C++ 封装 OpenCL 和 Vulkan 的底层 API,并实现多设备并发计算。这是一个比较深入的话题,涉及到 GPU 编程的底层细节和性能优化。希望通过今天的讲解,大家能够对 OpenCL 和 Vulkan 的计算着色器开发有更深入的理解。

第一部分:OpenCL 和 Vulkan 的基础概念回顾

在深入 C++ 封装之前,我们先快速回顾一下 OpenCL 和 Vulkan 的一些核心概念。

  • OpenCL (Open Computing Language):一个跨平台的异构并行计算标准,允许在 CPU、GPU 和其他加速器上执行代码。
  • Vulkan:一个低开销、跨平台的 3D 图形和计算 API,相比 OpenCL,它提供了更精细的控制权,但也意味着更高的复杂性。

两者都依赖于计算着色器(Compute Shader)来实现通用计算(GPGPU)。计算着色器是一段在 GPU 上执行的程序,可以并行处理大量数据。

第二部分:OpenCL 的 C++ 底层 API 封装

OpenCL 的 C API 比较繁琐,直接使用容易出错。因此,封装成 C++ 类可以提高代码的可读性和可维护性。

1. 核心对象封装

我们需要封装以下 OpenCL 核心对象:

  • cl_platform_id:平台(Platform)
  • cl_device_id:设备(Device)
  • cl_context:上下文(Context)
  • cl_command_queue:命令队列(Command Queue)
  • cl_program:程序(Program)
  • cl_kernel:内核(Kernel)
  • cl_mem:内存对象(Memory Object)

下面是一个简单的 OpenCLContext 类的示例:

#include <CL/cl.hpp>
#include <vector>
#include <iostream>

class OpenCLContext {
public:
    OpenCLContext() : platform(0), device(0), context(0), commandQueue(0) {}

    bool init() {
        std::vector<cl::Platform> platforms;
        cl::Platform::get(&platforms);
        if (platforms.empty()) {
            std::cerr << "No OpenCL platforms found." << std::endl;
            return false;
        }
        platform = platforms[0];
        std::cout << "Using platform: " << platform.getInfo<CL_PLATFORM_NAME>() << std::endl;

        std::vector<cl::Device> devices;
        platform.getDevices(CL_DEVICE_TYPE_GPU, &devices);
        if (devices.empty()) {
            std::cerr << "No OpenCL GPU devices found." << std::endl;
            return false;
        }
        device = devices[0];
        std::cout << "Using device: " << device.getInfo<CL_DEVICE_NAME>() << std::endl;

        context = cl::Context(device);
        commandQueue = cl::CommandQueue(context, device);

        return true;
    }

    cl::Program createProgram(const std::string& source) {
        cl::Program program(context, source);
        try {
            program.build({device});
        } catch (const cl::Error& err) {
            std::cerr << "Build failed: " << err.what() << "(" << err.err() << ")" << std::endl;
            std::cerr << "Build log:" << std::endl << program.getBuildInfo<CL_PROGRAM_BUILD_LOG>(device) << std::endl;
            throw;
        }
        return program;
    }

    cl::Kernel createKernel(const cl::Program& program, const std::string& kernelName) {
        return cl::Kernel(program, kernelName.c_str());
    }

    cl::Buffer createBuffer(size_t size, cl_mem_flags flags) {
        return cl::Buffer(context, flags, size);
    }

    void enqueueNDRangeKernel(const cl::Kernel& kernel, cl::NDRange globalSize, cl::NDRange localSize, const std::vector<cl::Event>* events = nullptr) {
        commandQueue.enqueueNDRangeKernel(kernel, cl::NullRange, globalSize, localSize, events);
    }

    void enqueueReadBuffer(const cl::Buffer& buffer, bool blocking, size_t offset, size_t size, void* ptr, const std::vector<cl::Event>* events = nullptr) {
        commandQueue.enqueueReadBuffer(buffer, blocking, offset, size, ptr, events);
    }

    void enqueueWriteBuffer(const cl::Buffer& buffer, bool blocking, size_t offset, size_t size, const void* ptr, const std::vector<cl::Event>* events = nullptr) {
        commandQueue.enqueueWriteBuffer(buffer, blocking, offset, size, size, ptr, events);
    }

    void finish() {
        commandQueue.finish();
    }

private:
    cl::Platform platform;
    cl::Device device;
    cl::Context context;
    cl::CommandQueue commandQueue;
};

这个类封装了 OpenCL 的初始化、程序编译、内核创建、内存对象创建、内核执行和数据传输等常用操作。注意错误处理使用了 try...catch 块来捕获 cl::Error 异常,并打印详细的错误信息。

2. 内存对象管理

内存对象(cl_mem)是 OpenCL 中数据传输的关键。我们需要封装内存对象的创建、读写操作。

// 在 OpenCLContext 类中添加
cl::Buffer createBuffer(size_t size, cl_mem_flags flags) {
    return cl::Buffer(context, flags, size);
}

void enqueueReadBuffer(const cl::Buffer& buffer, bool blocking, size_t offset, size_t size, void* ptr, const std::vector<cl::Event>* events = nullptr) {
    commandQueue.enqueueReadBuffer(buffer, blocking, offset, size, ptr, events);
}

void enqueueWriteBuffer(const cl::Buffer& buffer, bool blocking, size_t offset, size_t size, const void* ptr, const std::vector<cl::Event>* events = nullptr) {
    commandQueue.enqueueWriteBuffer(buffer, blocking, offset, size, size, ptr, events);
}

这些函数简化了内存对象的创建和数据传输过程。

3. 内核执行

内核执行是 OpenCL 计算的核心。我们需要封装内核的参数设置和执行。

// 在 OpenCLContext 类中添加
void enqueueNDRangeKernel(const cl::Kernel& kernel, cl::NDRange globalSize, cl::NDRange localSize, const std::vector<cl::Event>* events = nullptr) {
    commandQueue.enqueueNDRangeKernel(kernel, cl::NullRange, globalSize, localSize, events);
}

enqueueNDRangeKernel 函数简化了内核的执行过程,并允许指定全局和局部工作大小。

4. 示例:向量加法

下面是一个使用封装后的 OpenCL API 实现向量加法的示例:

#include <iostream>
#include <vector>
#include <CL/cl.hpp>

const std::string kernelSource = R"(
    __kernel void vector_add(__global const float *a, __global const float *b, __global float *result) {
        int i = get_global_id(0);
        result[i] = a[i] + b[i];
    }
)";

int main() {
    OpenCLContext context;
    if (!context.init()) {
        return 1;
    }

    size_t vectorSize = 1024;
    std::vector<float> a(vectorSize, 1.0f);
    std::vector<float> b(vectorSize, 2.0f);
    std::vector<float> result(vectorSize, 0.0f);

    // 创建 OpenCL 内存对象
    cl::Buffer bufferA = context.createBuffer(vectorSize * sizeof(float), CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR);
    cl::Buffer bufferB = context.createBuffer(vectorSize * sizeof(float), CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR);
    cl::Buffer bufferResult = context.createBuffer(vectorSize * sizeof(float), CL_MEM_WRITE_ONLY);

    // 将数据写入 OpenCL 内存对象
    context.enqueueWriteBuffer(bufferA, CL_TRUE, 0, vectorSize * sizeof(float), a.data());
    context.enqueueWriteBuffer(bufferB, CL_TRUE, 0, vectorSize * sizeof(float), b.data());

    // 创建 OpenCL 程序和内核
    cl::Program program = context.createProgram(kernelSource);
    cl::Kernel kernel = context.createKernel(program, "vector_add");

    // 设置内核参数
    kernel.setArg(0, bufferA);
    kernel.setArg(1, bufferB);
    kernel.setArg(2, bufferResult);

    // 执行内核
    cl::NDRange globalSize(vectorSize);
    cl::NDRange localSize(64); // 选择合适的 localSize
    context.enqueueNDRangeKernel(kernel, globalSize, localSize);

    // 将结果读回主机内存
    context.enqueueReadBuffer(bufferResult, CL_TRUE, 0, vectorSize * sizeof(float), result.data());

    // 验证结果
    for (size_t i = 0; i < vectorSize; ++i) {
        if (result[i] != 3.0f) {
            std::cerr << "Error: result[" << i << "] = " << result[i] << std::endl;
            return 1;
        }
    }

    std::cout << "Vector addition successful!" << std::endl;

    return 0;
}

这个示例展示了如何使用封装后的 OpenCL API 实现一个简单的向量加法程序。

第三部分:Vulkan 的 C++ 底层 API 封装

Vulkan 的 API 更加底层,需要更多的代码来完成相同的任务。C++ 封装可以大大简化 Vulkan 的使用。

1. 核心对象封装

我们需要封装以下 Vulkan 核心对象:

  • VkInstance:实例(Instance)
  • VkPhysicalDevice:物理设备(Physical Device)
  • VkDevice:逻辑设备(Logical Device)
  • VkQueue:队列(Queue)
  • VkCommandPool:命令池(Command Pool)
  • VkCommandBuffer:命令缓冲区(Command Buffer)
  • VkShaderModule:着色器模块(Shader Module)
  • VkPipelineLayout:管线布局(Pipeline Layout)
  • VkPipeline:管线(Pipeline)
  • VkBuffer:缓冲区(Buffer)
  • VkDeviceMemory:设备内存(Device Memory)
  • VkDescriptorSetLayout:描述符集布局(Descriptor Set Layout)
  • VkDescriptorPool:描述符池(Descriptor Pool)
  • VkDescriptorSet:描述符集(Descriptor Set)

下面是一个简化的 VulkanContext 类示例:

#include <vulkan/vulkan.h>
#include <iostream>
#include <vector>

class VulkanContext {
public:
    VulkanContext() : instance(VK_NULL_HANDLE), physicalDevice(VK_NULL_HANDLE), device(VK_NULL_HANDLE), queue(VK_NULL_HANDLE), commandPool(VK_NULL_HANDLE) {}

    bool init() {
        // 1. 创建 Instance
        VkApplicationInfo appInfo = {};
        appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
        appInfo.pApplicationName = "Compute Shader Example";
        appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
        appInfo.pEngineName = "No Engine";
        appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
        appInfo.apiVersion = VK_API_VERSION_1_0;

        VkInstanceCreateInfo createInfo = {};
        createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
        createInfo.pApplicationInfo = &appInfo;

        std::vector<const char*> requiredExtensions = {
            VK_KHR_SURFACE_EXTENSION_NAME,
            VK_KHR_PLATFORM_SURFACE_EXTENSION_NAME // 替换为你的平台扩展
        };
        createInfo.enabledExtensionCount = static_cast<uint32_t>(requiredExtensions.size());
        createInfo.ppEnabledExtensionNames = requiredExtensions.data();

        VkResult result = vkCreateInstance(&createInfo, nullptr, &instance);
        if (result != VK_SUCCESS) {
            std::cerr << "Failed to create instance!" << std::endl;
            return false;
        }

        // 2. 选择 Physical Device
        uint32_t deviceCount = 0;
        vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);
        if (deviceCount == 0) {
            std::cerr << "Failed to find GPUs with Vulkan support!" << std::endl;
            return false;
        }
        std::vector<VkPhysicalDevice> devices(deviceCount);
        vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());

        for (const auto& device : devices) {
            VkPhysicalDeviceProperties deviceProperties;
            vkGetPhysicalDeviceProperties(device, &deviceProperties);
            if (deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) {
                physicalDevice = device;
                break;
            }
        }

        if (physicalDevice == VK_NULL_HANDLE) {
            physicalDevice = devices[0]; // 如果没有独立显卡,选择第一个
        }

        // 3. 创建 Logical Device
        float queuePriority = 1.0f;
        VkDeviceQueueCreateInfo queueCreateInfo = {};
        queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
        queueCreateInfo.queueFamilyIndex = 0; // 假设只有一个队列族
        queueCreateInfo.queueCount = 1;
        queueCreateInfo.pQueuePriorities = &queuePriority;

        VkPhysicalDeviceFeatures deviceFeatures = {};

        VkDeviceCreateInfo deviceCreateInfo = {};
        deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
        deviceCreateInfo.pQueueCreateInfos = &queueCreateInfo;
        deviceCreateInfo.queueCreateInfoCount = 1;
        deviceCreateInfo.pEnabledFeatures = &deviceFeatures;

        result = vkCreateDevice(physicalDevice, &deviceCreateInfo, nullptr, &device);
        if (result != VK_SUCCESS) {
            std::cerr << "Failed to create logical device!" << std::endl;
            return false;
        }

        // 4. 获取 Queue
        vkGetDeviceQueue(device, 0, 0, &queue);

        // 5. 创建 Command Pool
        VkCommandPoolCreateInfo poolInfo = {};
        poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
        poolInfo.queueFamilyIndex = 0; // 假设只有一个队列族
        poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;

        result = vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool);
        if (result != VK_SUCCESS) {
            std::cerr << "Failed to create command pool!" << std::endl;
            return false;
        }

        return true;
    }

    VkShaderModule createShaderModule(const std::vector<char>& code) {
        VkShaderModuleCreateInfo createInfo = {};
        createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
        createInfo.codeSize = code.size();
        createInfo.pCode = reinterpret_cast<const uint32_t*>(code.data());

        VkShaderModule shaderModule;
        if (vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) {
            throw std::runtime_error("failed to create shader module!");
        }

        return shaderModule;
    }

    VkCommandBuffer beginSingleTimeCommands() {
        VkCommandBufferAllocateInfo allocInfo = {};
        allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
        allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
        allocInfo.commandPool = commandPool;
        allocInfo.commandBufferCount = 1;

        VkCommandBuffer commandBuffer;
        vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer);

        VkCommandBufferBeginInfo beginInfo = {};
        beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
        beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;

        vkBeginCommandBuffer(commandBuffer, &beginInfo);

        return commandBuffer;
    }

    void endSingleTimeCommands(VkCommandBuffer commandBuffer) {
        vkEndCommandBuffer(commandBuffer);

        VkSubmitInfo submitInfo = {};
        submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
        submitInfo.commandBufferCount = 1;
        submitInfo.pCommandBuffers = &commandBuffer;

        vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE);
        vkQueueWaitIdle(queue);

        vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
    }

    void cleanup() {
        if (commandPool != VK_NULL_HANDLE) {
            vkDestroyCommandPool(device, commandPool, nullptr);
        }
        if (device != VK_NULL_HANDLE) {
            vkDestroyDevice(device, nullptr);
        }
        if (instance != VK_NULL_HANDLE) {
            vkDestroyInstance(instance, nullptr);
        }
    }

private:
    VkInstance instance;
    VkPhysicalDevice physicalDevice;
    VkDevice device;
    VkQueue queue;
    VkCommandPool commandPool;
};

这个类封装了 Vulkan 的初始化过程,包括实例创建、物理设备选择、逻辑设备创建、队列获取和命令池创建。同样,错误处理也至关重要。

2. 缓冲区和内存管理

Vulkan 的缓冲区和内存管理非常复杂,需要手动分配和绑定内存。

// 在 VulkanContext 类中添加
uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) {
    VkPhysicalDeviceMemoryProperties memProperties;
    vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);

    for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {
        if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
            return i;
        }
    }

    throw std::runtime_error("failed to find suitable memory type!");
}

void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer& buffer, VkDeviceMemory& bufferMemory) {
    VkBufferCreateInfo bufferInfo = {};
    bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
    bufferInfo.size = size;
    bufferInfo.usage = usage;
    bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

    if (vkCreateBuffer(device, &bufferInfo, nullptr, &buffer) != VK_SUCCESS) {
        throw std::runtime_error("failed to create buffer!");
    }

    VkMemoryRequirements memRequirements;
    vkGetBufferMemoryRequirements(device, buffer, &memRequirements);

    VkMemoryAllocateInfo allocInfo = {};
    allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
    allocInfo.allocationSize = memRequirements.size;
    allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties);

    if (vkAllocateMemory(device, &allocInfo, nullptr, &bufferMemory) != VK_SUCCESS) {
        throw std::runtime_error("failed to allocate buffer memory!");
    }

    vkBindBufferMemory(device, buffer, bufferMemory, 0);
}

findMemoryType 函数用于查找合适的内存类型,createBuffer 函数用于创建缓冲区和分配内存。

3. 计算管线创建

Vulkan 的计算管线需要手动创建,包括着色器模块、管线布局和管线对象。

// 在 VulkanContext 类中添加
VkPipeline createComputePipeline(VkShaderModule shaderModule, VkPipelineLayout& pipelineLayout) {
    VkPipelineShaderStageCreateInfo shaderStageInfo = {};
    shaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
    shaderStageInfo.stage = VK_SHADER_STAGE_COMPUTE_BIT;
    shaderStageInfo.module = shaderModule;
    shaderStageInfo.pName = "main";

    VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
    pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
    pipelineLayoutInfo.setLayoutCount = 0; // Optional
    pipelineLayoutInfo.pSetLayouts = nullptr; // Optional
    pipelineLayoutInfo.pushConstantRangeCount = 0; // Optional
    pipelineLayoutInfo.pPushConstantRanges = nullptr; // Optional

    if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) {
        throw std::runtime_error("failed to create pipeline layout!");
    }

    VkComputePipelineCreateInfo pipelineInfo = {};
    pipelineInfo.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO;
    pipelineInfo.stage = shaderStageInfo;
    pipelineInfo.layout = pipelineLayout;
    pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; // Optional
    pipelineInfo.basePipelineIndex = -1; // Optional

    VkPipeline pipeline;
    if (vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline) != VK_SUCCESS) {
        throw std::runtime_error("failed to create compute pipeline!");
    }

    return pipeline;
}

这个函数简化了计算管线的创建过程。

4. 示例:向量加法

下面是一个使用封装后的 Vulkan API 实现向量加法的示例:

#include <iostream>
#include <fstream>
#include <vector>
#include <vulkan/vulkan.h>

// 读取着色器代码
std::vector<char> readFile(const std::string& filename) {
    std::ifstream file(filename, std::ios::ate | std::ios::binary);

    if (!file.is_open()) {
        throw std::runtime_error("failed to open file!");
    }

    size_t fileSize = (size_t) file.tellg();
    std::vector<char> buffer(fileSize);

    file.seekg(0);
    file.read(buffer.data(), fileSize);

    file.close();

    return buffer;
}

int main() {
    VulkanContext context;
    if (!context.init()) {
        return 1;
    }

    size_t vectorSize = 1024;
    std::vector<float> a(vectorSize, 1.0f);
    std::vector<float> b(vectorSize, 2.0f);
    std::vector<float> result(vectorSize, 0.0f);

    // 创建缓冲区
    VkBuffer bufferA, bufferB, bufferResult;
    VkDeviceMemory memoryA, memoryB, memoryResult;
    VkDeviceSize bufferSize = vectorSize * sizeof(float);

    context.createBuffer(bufferSize, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, bufferA, memoryA);
    context.createBuffer(bufferSize, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, bufferB, memoryB);
    context.createBuffer(bufferSize, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, bufferResult, memoryResult);

    // 将数据写入缓冲区
    void* data;
    vkMapMemory(context.device, memoryA, 0, bufferSize, 0, &data);
    memcpy(data, a.data(), bufferSize);
    vkUnmapMemory(context.device, memoryA);

    vkMapMemory(context.device, memoryB, 0, bufferSize, 0, &data);
    memcpy(data, b.data(), bufferSize);
    vkUnmapMemory(context.device, memoryB);

    // 创建着色器模块
    std::vector<char> shaderCode = readFile("vector_add.comp.spv"); // 编译好的 SPIR-V 着色器
    VkShaderModule shaderModule = context.createShaderModule(shaderCode);

    // 创建管线
    VkPipelineLayout pipelineLayout;
    VkPipeline computePipeline = context.createComputePipeline(shaderModule, pipelineLayout);

    // 创建命令缓冲区
    VkCommandBuffer commandBuffer = context.beginSingleTimeCommands();

    // 绑定管线
    vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, computePipeline);

    // 调度计算
    vkCmdDispatch(commandBuffer, (uint32_t)vectorSize / 64, 1, 1); // 选择合适的 workgroup size

    // 结束命令缓冲区
    context.endSingleTimeCommands(commandBuffer);

    // 将结果读回主机内存
    vkMapMemory(context.device, memoryResult, 0, bufferSize, 0, &data);
    memcpy(result.data(), data, bufferSize);
    vkUnmapMemory(context.device, memoryResult);

    // 验证结果
    for (size_t i = 0; i < vectorSize; ++i) {
        if (result[i] != 3.0f) {
            std::cerr << "Error: result[" << i << "] = " << result[i] << std::endl;
            return 1;
        }
    }

    std::cout << "Vulkan vector addition successful!" << std::endl;

    // 清理资源
    vkDestroyBuffer(context.device, bufferA, nullptr);
    vkFreeMemory(context.device, memoryA, nullptr);
    vkDestroyBuffer(context.device, bufferB, nullptr);
    vkFreeMemory(context.device, memoryB, nullptr);
    vkDestroyBuffer(context.device, bufferResult, nullptr);
    vkFreeMemory(context.device, memoryResult, nullptr);
    vkDestroyPipeline(context.device, computePipeline, nullptr);
    vkDestroyPipelineLayout(context.device, pipelineLayout, nullptr);
    vkDestroyShaderModule(context.device, shaderModule, nullptr);
    context.cleanup();

    return 0;
}

这个示例展示了如何使用封装后的 Vulkan API 实现一个简单的向量加法程序。注意,你需要一个编译好的 SPIR-V 着色器文件 vector_add.comp.spv

第五部分:多设备并发管理

当系统中有多个 OpenCL 或 Vulkan 设备时,我们可以利用它们进行并发计算,提高整体性能。

1. 设备选择

在 OpenCL 中,可以使用 cl::Platform::getDevices 函数获取所有设备,并根据设备类型(CPU、GPU 等)和性能指标选择合适的设备。在 Vulkan 中,可以使用 vkEnumeratePhysicalDevices 函数获取所有物理设备,并根据设备属性选择。

2. 任务划分

将计算任务划分为多个子任务,每个子任务分配给一个设备执行。任务划分的粒度需要根据任务的特性和设备的性能进行调整。

3. 数据分配

将输入数据分配给各个设备,确保每个设备都有足够的本地数据进行计算。可以使用数据复制或数据分片的方式进行数据分配。

4. 并发执行

使用多线程或异步任务的方式并发执行各个设备的计算任务。可以使用 C++ 的 std::threadstd::async 函数创建并发任务。

5. 结果合并

等待所有设备完成计算后,将各个设备的结果合并成最终结果。可以使用锁或原子操作来保证结果合并的正确性。

6. 示例:OpenCL 多设备向量加法

#include <iostream>
#include <vector>
#include <CL/cl.hpp>
#include <thread>

const std::string kernelSource = R"(
    __kernel void vector_add(__global const float *a, __global const float *b, __global float *result) {
        int i = get_global_id(0);
        result[i] = a[i] + b[i];
    }
)";

void vectorAdd(cl::Device device, const std::vector<float>& a, const std::vector<float>& b, std::vector<float>& result, size_t offset, size_t size) {
    cl::Context context(device);
    cl::CommandQueue commandQueue(context, device);

    // 创建 OpenCL 内存对象
    cl::Buffer bufferA(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, size * sizeof(float), (void*)(a.data() + offset));
    cl::Buffer bufferB(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, size * sizeof(float), (void*)(b.data() + offset));
    cl::Buffer bufferResult(context, CL_MEM_WRITE_ONLY, size * sizeof(float));

    // 创建 OpenCL 程序和内核
    cl::Program program(context, kernelSource);
    program.build({device});
    cl::Kernel kernel(program, "vector_add");

    // 设置内核参数
    kernel.setArg(0, bufferA);
    kernel.setArg(1, bufferB);
    kernel.setArg(2, bufferResult);

    // 执行内核
    cl::NDRange globalSize(size);
    cl::NDRange localSize(64); // 选择合适的 localSize
    commandQueue.enqueueNDRangeKernel(kernel, cl::NullRange, globalSize, localSize);

    // 将结果读回主机内存
    commandQueue.enqueueReadBuffer(bufferResult, CL_TRUE, 0, size * sizeof(float), result.data() + offset);
    commandQueue.finish();
}

int main() {
    std::vector<cl::Platform> platforms;
    cl::Platform::get(&platforms);
    if (platforms.empty()) {
        std::cerr << "No OpenCL platforms found." << std::endl;
        return 1;
    }
    cl::Platform platform = platforms[0];

    std::vector<cl::Device> devices;
    platform.getDevices(CL_DEVICE_TYPE_GPU, &devices);
    if (devices.empty()) {
        std::cerr << "No OpenCL GPU devices found." << std::endl;
        return 1;
    }

    size_t vectorSize = 1024 * 1024;
    std::vector<float> a(vectorSize, 1.0f);
    std::vector<float> b(vectorSize, 2.0f);
    std::vector<float> result(vectorSize, 0.0f);

    size_t numDevices = devices.size();
    size_t chunkSize = vectorSize / numDevices;

    std::vector<std::thread> threads;
    for (size_t i = 0; i < numDevices; ++i) {
        size_t offset = i * chunkSize;
        size_t size = chunkSize;
        if (i == numDevices - 1) {
            size = vectorSize - offset; // 处理最后一个chunk
        }
        threads.emplace_back(vectorAdd, devices[i], std::ref(a), std::ref(b), std::ref(result), offset, size);
    }

    for (auto& thread : threads) {
        thread.join();
    }

    // 验证结果
    for (size_t i = 0; i < vectorSize; ++i) {
        if (result[i] != 3.0f) {
            std::cerr << "Error: result[" << i << "] = " << result[i] << std::endl;
            return 1;
        }
    }

    std::cout << "Multi-device vector addition successful!" << std::endl;

    return 0;
}

这个示例展示了如何使用多个 OpenCL 设备并发执行向量加法。

总结:API封装与并行计算的策略

本文介绍了如何使用 C++ 封装 OpenCL 和 Vulkan 的底层 API,并通过一个向量加法的例子展示了如何使用封装后的 API 进行计算。同时,还讨论了如何利用多设备进行并发计算,以提高整体性能。希望这些内容能帮助大家更好地理解和应用 OpenCL 和 Vulkan 的计算着色器。

更多IT精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注