前言

笔者尝试在开源鸿蒙上跑通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]:按行访问矩阵A

b[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

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐