在OpenHarmony上使用vulkan图形接口做矩阵运算
前言
笔者尝试在开源鸿蒙上跑通vulkan这个图形接口,目前OpenHarmony社区在三方库上只移植了vulkan-loader和vulkan-headers这两个开源项目。
因此自己尝试去编译vulkan的驱动和移植vulkan sdk,并写了一个简单的demo来验证。vulkan驱动和sdk相关的在未来会进行分享。
本篇文章目前主要分享vulkan图形接口如何做矩阵运算。
本demo采用Vulkan API实现矩阵乘法运算,并使用计算着色器实现并行计算。具体可看https://gitee.com/linyongwei/vulkan_demo/tree/master/02_matrix_compute_ohos
主程序代码
1. Vulkan初始化
- 创建Instance、PhysicalDevice和LogicalDevice
- 启用计算队列(VK_QUEUE_COMPUTE_BIT)
- 注意:代码中启用了验证层VK_LAYER_KHRONOS_validation
2. 矩阵数据结构
struct Matrix {
std::vector<float> data; // CPU端数据
VkBuffer buffer; // GPU缓冲区
VkDeviceMemory memory; // 设备内存
};
3. 缓冲区管理
createBuffer()函数完成:
- 创建VkBuffer
- 分配设备内存(选择HOST_VISIBLE且COHERENT的内存类型)
- 数据映射拷贝(通过vkMapMemory实现CPU到GPU数据传输)
4. 计算管线创建
- 加载预编译的SPIR-V着色器(matrix.comp.spv)
- 创建描述符集布局(绑定3个存储缓冲区)
- 创建计算管线布局和计算管线
5. 资源绑定
- 创建描述符池并分配描述符集
- 将三个矩阵缓冲区绑定到描述符集:
bufferInfos[0] = a.buffer; // 输入矩阵A bufferInfos[1] = b.buffer; // 输入矩阵B bufferInfos[2] = c.buffer; // 输出矩阵C
6. 命令执行流程
- 创建命令池和命令缓冲区
- 录制命令:
vkCmdBindPipeline // 绑定计算管线 vkCmdBindDescriptorSets // 绑定描述符集 vkCmdDispatch(MATRIX_SIZE/16, MATRIX_SIZE/16, 1) // 调度计算任
7. 同步机制
- 使用VkFence确保计算完成
- 通过vkWaitForFences等待命令执行完毕
8. 性能测量
- 使用chrono库测量GPU计算耗时
- 验证第一个元素的正确性(预期值512 = 256*2)
9. 资源清理
- 按Vulkan规范逆向销毁所有创建的对象
- 执行流程:初始化Vulkan → 创建矩阵缓冲区 → 构建计算管线 → 提交计算任务 → 同步获取结果 → 验证输出 → 资源释放
10. 总结:
- 使用计算着色器实现并行矩阵乘法
- 工作组配置为16x16(对应着色器中的local_size)
- 采用主机可见内存便于数据传输
- 实现基础的错误检查(通过throw)
着色器代码
1.着色器配置
glsl:
#version 450
layout(local_size_x = 16, local_size_y = 16) in;
工作组配置:每个工作组包含16x16=256个并行线程
执行粒度:与主程序中的vkCmdDispatch(MATRIX_SIZE/16, MATRIX_SIZE/16, 1)配合,共调度 (256/16)^2 = 256个工作组覆盖整个矩阵
2. 存储缓冲区绑定
layout(binding = 0) buffer MatrixA { float a[]; }; // 输入矩阵A
layout(binding = 1) buffer MatrixB { float b[]; }; // 输入矩阵B
layout(binding = 2) buffer MatrixC { float c[]; }; // 输出矩阵C
绑定索引:与主程序中的描述符集布局完全对应(绑定点0-2)
数据布局:使用线性数组存储二维矩阵数据(行优先)
3. 核心算法
void main() {
uint row = gl_GlobalInvocationID.x; // 全局行索引
uint col = gl_GlobalInvocationID.y; // 全局列索引
float sum = 0.0;
for (uint i = 0; i < 256; i++) {
sum += a[row * 256 + i] * b[i * 256 + col];
}
c[row * 256 + col] = sum;
}
并行策略:每个线程计算输出矩阵的一个元素
计算模式:
a[row][i]:按行访问矩阵Ab[i][col]:按列访问矩阵B(注意内存访问模式问题)
符合矩阵乘法定义:C[row][col] = Σ(A[row][k] * B[k][col])
4. 与main函数对应
着色器部分 | 主程序对应实现 |
---|---|
local_size_x/y | vkCmdDispatch的分组参数 |
binding索引 | 描述符集布局的绑定顺序 |
缓冲区结构 | Matrix结构体的data存储顺序 |
5. 性能注意事项
内存访问优化:矩阵B的列访问模式可能导致非连续内存访问,可考虑:
-
使用共享内存缓存数据
-
转置矩阵B使其按行存储
-
循环展开:固定循环次数256便于编译器优化
-
工作组大小:16x16是常见优化选择,需根据GPU架构调整
6. 验证测试
当输入矩阵A全为1,B全为2时:
每个输出元素 = Σ(12) 从i=0到255 → 2562 = 512
与主程序中的std::cout << "Result[0][0] = " << result[0]验证一致
该着色器实现了基础的并行矩阵乘法,可通过优化内存访问模式和利用共享内存进一步提升性能。
编译运行
可看log.txt
更多推荐
所有评论(0)