资讯动态

OpenHarmony上用Vulkan驱动Mali G720做端侧推理实战指南

发布时间:2026/10/13 9:08:58 来源:尧图企业网站定制
如果你手里正捏着一块 cix p1 开发板系统刷的是 OpenHarmony 6.0 Release又不甘心只拿 NPU 跑官方脚本想绕过那一堆“只能跑内置模型”的限制自己用 Vulkan 把 Mali G720 这张 GPU 拉起来做端侧推理那这篇东西就是给你看的施工笔记。先交代背景cix p1 这块板子主芯片自带 RISC-V 核心、硬件 NPU 和 ARM Mali GPUOpenHarmony 6.0R 在这类板子上能正常起图形栈但真正把 GPU 的计算能力用起来的人不多。我前后折腾了大概三周把 Vulkan 计算管线从零跑通到能稳定推理这中间踩了不少文档里不会写的坑。本文会把完整思路、环境搭建、核心代码结构、推理框架选择和排查经验全部摊开适合刚接触 OpenHarmony 图形栈、或者准备在 RISC-V Mali 平台上做 GPU 推理的开发者参考。基础薄弱也没关系前半部分我把原理讲得很直白。1. 项目整体拆解为什么要走 Vulkan 这条路1.1 cix p1 和 OpenHarmony 6.0R 的真实情况网上很多文章把这类开发板吹得很玄实际拿在手里就是一块带 Type-C 供电的 ARM/RISC-V 混合架构单板。cix p1 的主芯片集成了多核 RISC-V CPU、自研 NPU 和 Mali G720 GPUOpenHarmony 6.0 Release 是当前比较稳定的版本分支官方对三方开发板的适配主要保证基础系统、图形和多媒体链路能跑。GPU 这块系统中会挂上 Mali 的驱动模块OpenHarmony 的图形栈也带了 Vulkan loader所以理论上 Vulkan 是可以直接枚举到设备的。但“理论上可以”和“实际上能用”之间差着十万八千里。OpenHarmony 的 Vulkan 支持通常只保证渲染场景很多板卡固件里的 Vulkan 版本是打了折扣的要么 loader 版本老旧要么 ICDInstallable Client Driver没有正确注册要么驱动只实现了部分扩展。加上 RISC-V 架构的生态本来就比 ARM 稀疏很多工具链问题得自己动手解决。我这次的目标非常明确用 GPU 做端侧推理不是做图形渲染。推理场景对计算管线、显存管理和同步的要求更高GPU 厂商在驱动上对计算能力的支持程度直接决定了能不能跑。选择 Vulkan 作为入口其实是经过对比之后最稳妥的一条路线。1.2 为什么是 Vulkan而不是 OpenCL 或者 GPU 内的其他写法做 GPU 通用计算通常有四条路可以选OpenCL、OpenGL ES 的 compute shader、NPU 专用接口、Vulkan。我在 cix p1 上把这四条路都摸了一遍结论很明确。OpenCL 在 Mali 驱动上其实一直有人用但 OpenHarmony 的图形栈对 OpenCL 的支持非常暧昧很多版本根本没有装上 libOpenCL.so或者装上之后 ICD 文件和 Vulkan 的冲突实测枚举不到 device基本等于没法用。OpenGL ES 的 compute shader 理论上能通过 EGL 走起来但版本限制多、同步模型简陋跑复杂模型时要么频繁 flush 要么出现数据竞争性能没法压。NPU 接口是厂商主推的跑内置的那几个 CV 模型确实爽但一旦模型里出现不支持的算子就得等厂商更新工具链这是个黑盒不是你能控制的。Vulkan 就不一样这是一个跨厂商的开放标准Mali 驱动对 Vulkan 计算管线的支持是相对完整的而且后续如果把模型迁移到其他平台Vulkan 代码几乎可以原样复用。一句话总结我的选型逻辑Vulkan 是 cix p1 上唯一“既能脱离图形上下文跑纯计算又是开放标准、驱动支持较完整”的方案。这属于关键路径选型后面所有工作都建立在这个决定之上。1.3 整体技术栈和知识准备这次项目的技术栈分为三层底层是 OpenHarmony 6.0R 系统自带的 Mali G720 驱动和 Vulkan loader中间是 Vulkan 计算管线负责把数据搬运到 GPU、调度 compute shader、拿回结果上层是推理框架和模型我在实验中先拿 NCNN 的 Vulkan 后端做完整性验证再拆出几个核心算子自己实现方便定位问题。如果你准备照着做建议先补齐几个知识点Vulkan 的 pipeline 和 command buffer 生命周期、SPIR-V 与 GLSL 的对应关系、GPU 端内存模型里的 host visible / device local 区别以及 compute shader 的 workgroup 概念。上面这些不要求深入源码但至少要知道每个对象是干什么的。后面每一节我都会把对应概念用大白话交代一遍。2. 环境搭建先把开发链跑通2.1 工具链选择与交叉编译准备OpenHarmony 的 Native 开发和 Linux 主机开发最大的区别是 sysroot 和工具链。官方提供的是基于 Clang 的交叉编译工具链目标三元组在 6.0R 上一般是aarch64-linux-ohos这跟常见的aarch64-linux-gnu有差异主要体现在 C 运行库的符号和头文件路径上。一开始我图省事直接用系统里的普通 aarch64 交叉编译器编出来的 so 在板子上加载时报各种 undefined symbol排查了半天最后乖乖去 SDK 里取 clang 和对应的 sysroot。具体工具链清单如下组件用途说明ohos-sdk 中的 clangC/C 交叉编译必须带--targetaarch64-linux-ohosohos-sdk 中的 sysroot头文件与运行库路径一般在 sdk 的 native/sysroot 下CMake Ninja构建系统版本分别用 3.20 和 1.10 以上即可glslangValidatorGLSL 转 SPIR-V版本不要太新兼容 Vulkan 1.0/1.1 即可Vulkan-Headers开发头文件从官方仓库取 release 版本这里有个很多人会忽略的坑OpenHarmony 的 sysroot 自带一套 vulkan 平台头文件但版本往往落后于 Khronos 官方仓库。我的做法是先用官方 khronos 的 Vulkan-Headers 编库编译期指定 include 顺序把官方头文件放在前面运行时再根据板子上的 loader 实际导出的函数指针去加载这样两头都能照顾到。交叉编译的关键命令示意如下clang \ --targetaarch64-linux-ohos \ --sysroot$OHOS_SYSROOT \ -fPIC -shared -stdc17 \ -I$VULKAN_HEADERS/include \ -I$OHOS_SYSROOT/usr/include \ inference_vk.cpp -o libinfer_vk.so不要直接在命令行上拼那么长老老实实写一份 toolchain.cmake把 CMAKE_SYSTEM_NAME 设成 GenericCMAKE_C_COMPILER 指到 SDK 里的 clangC 运行库相关的标志位尽量少动。OpenHarmony 的 libc 对 C17 支持没问题但强异常相关的特性在板子上实测有些边缘行为不一致保险起见代码里少用需要 RTTI 的第三方库。2.2 确认 Mali G720 的 Vulkan 驱动是否可用刷完机之后第一件事不是写推理逻辑而是确认板上到底能不能枚举出 Mali G720。OpenHarmony 系统里 Vulkan loader 的加载路径和其他 Linux 发行版不太一样ICD 文件一般放在 vendor 分区或者 system 的特定目录。我判断驱动的步骤很简单在板子上找包含vulkan或者mali关键词的 so 文件确认驱动模块已经打包进系统写一个 20 行的枚举程序调用vkEnumerateInstanceExtensionProperties和vkEnumeratePhysicalDevices看枚举结果里VkPhysicalDeviceProperties.deviceName是不是Mali-G720同时确认apiVersion和driverVersion。这个枚举程序建议保留后面所有开发阶段都有用每次系统更新或者驱动替换之后先跑一遍确认环境没变。我在实际测试中发现OpenHarmony 6.0R 的某个固件组合下ICD 虽然存在但 loader 找不到它原因是没有把 ICD 的 json 路径写进系统的 Vulkan 配置里。这个问题用代码排查很费劲直接在系统目录里搜索.json文件名最快找到之后检查它的library_path字段是否指向正确路径即可。如果枚举正常apiVersion一般是 1.1 或者 1.2。这个版本号很重要直接决定了你能用哪些扩展。比如 subgroup 相关的能力在 Vulkan 1.1 里要看扩展在 1.2 里要看核心特性后续 shader 写法要跟着版本走。2.3 第一条 Vulkan 计算管线的验证方法环境确认之后我先写了一个非常朴素的 compute pipeline一个 shader 把输入 buffer 的每个元素加 1写回输出 buffer。这个步骤不是浪费时间它能把整条链路的所有环节都暴露出来包括实例创建、物理设备选择、队列获取、内存分配、descriptor set 绑定、shader 编译和 command buffer 提交。验证过程中发现两个极具迷惑性的现象。第一个是vkCreateShaderModule一直报VK_ERROR_INVALID_SHADER_NIR当时我以为是交叉编译工具链的问题查了半天才发现是 glslangValidator 版本太新默认生成的 SPIR-V 带了驱动不认识的扩展。解决办法是编译时加参数把目标环境限制到 vulkan1.0glslangValidator -V -S comp --target-env vulkan1.0 shader.comp -o shader.spv第二个现象是首次vkQueueSubmit后卡死看起来像死锁其实是没调用vkQueueWaitIdle就直接在 CPU 端读 bufferCPU 和 GPU 之间完全没有同步。这个不算 bug是我对 Vulkan 的异步模型还不够敏感但也说明验证程序里必须把同步逻辑从一开始就写对别偷懒。验证通过的标准很直接输入 buffer 里填 1 到 10输出 buffer 里读到 2 到 11连续跑一百次结果稳定。做到这一步Vulkan 计算链路在 cix p1 上就算通了接下来才是真正的推理开发。3. Vulkan 计算管线的核心实现3.1 设备、队列族和命令缓冲的选择Vulkan 里没有“直接把任务丢给 GPU”这种简单操作所有计算都必须先经过设备、队列、命令缓冲层层组织。cix p1 上的 Mali G720 一般会暴露两个队列族一个 graphicscompute一个 compute-only。推理场景用 compute-only 队列更干净可以避免和图形任务混在一起减少调度干扰。但要注意驱动上 compute-only 队列的queueCount很可能只有 1这意味着没有并发提交的空间所有推理请求必须排队。我在代码里对队列族的筛选逻辑是这样的先枚举所有队列族找到支持VK_QUEUE_COMPUTE_BIT且不支持VK_QUEUE_GRAPHICS_BIT的作为第一优先如果不存在退而求其次选 graphicscompute。这个策略在 cix p1 上实测稳定也方便以后迁移到别的板卡。命令缓冲建议用 secondary 还是 primary推理场景直接使用 primary command buffer每个推理请求生成一条独立的 command buffer记录一次完整的 dispatch 流程。这里的关键是 command buffer 的分配策略不要每次都调vkAllocateCommandBuffers那样会在驱动层造成不小的开销正确做法是预分配一个 command pool按推理请求数量循环复用。Mali 驱动对 command buffer 的管理不算激进频繁分配会造成可感知的帧率抖动。另外Vulkan 对象创建时大量使用pAllocator参数很多示例代码里直接传 nullptr但在长连接设备上我建议用自己实现的分配器做对象计数方便最终排查内存泄漏。3.2 Shader 编写GLSL 与 SPIR-V 的实战组合推理计算绝大多数是矩阵乘、卷积和逐元素操作这些在 GLSL compute shader 里写起来很直接。核心是利用 SSBOShader Storage Buffer Object让 CPU 端和 GPU 端共享一块显存里的数据。以最典型的向量加为例shader 长这样#version 450 layout(local_size_x 64, local_size_y 1, local_size_z 1) in; layout(set 0, binding 0) buffer InputBuf { float data[]; } inputBuf; layout(set 0, binding 1) buffer OutputBuf { float data[]; } outputBuf; void main() { uint idx gl_GlobalInvocationID.x; outputBuf.data[idx] inputBuf.data[idx] 1.0; }注意layout(local_size_x 64)。这个 64 不是拍脑袋定的通常需要先查询设备的maxComputeWorkGroupSize和maxComputeWorkGroupInvocations。Mali G720 的典型限制是单维 workgroup 不超过 256总 invocation 数 512 左右不同驱动版本略有差异。取 64 是经验值太小浪费并发太大容易撞上局部存储和寄存器限制。GLSL 转 SPIR-V 时有个细节值得专门提一下不要把binding号和set号写死在 shader 里之后就忘掉Vulkan 里 descriptor set 布局必须和 shader 里的声明严格一致。比如 shader 里声明了 binding 0 和 1那么 CPU 端创建 descriptor set 时也必须按这个顺序和类型来写错一个就报 validation error。我还是建议开发阶段打开 validation layers它能在提交前把这类错误报出来节省大量时间。3.3 计算管线、DescriptorSet 与调度逻辑计算管线的创建是一个繁琐的过程本质上就是把 shader、pipeline layout 和特殊常量打包成一个可执行对象。代码结构上我通常封装成一个ComputePipeline类里面按顺序完成五件事创建 shader module、创建 descriptor set layout、创建 pipeline layout、创建 pipeline cache、创建 compute pipeline。调度逻辑是整个推理的核心它由三段组成准备数据、派发计算、回收结果。准备数据是把输入从 CPU 内存拷贝到 GPU buffer派发计算是记录一条 command buffer 并提交到队列回收结果是同步完成后把 GPU buffer 拷回 CPU 内存。整个流程的代码骨架如下// 计算 dispatch 的 workgroup 数量 uint32_t total inputElements; uint32_t groupSize 64; uint32_t groups (total groupSize - 1) / groupSize; vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, layout, 0, 1, descSet, 0, nullptr); vkCmdDispatch(cmd, groups, 1, 1);派发之后需要处理同步。GPU 上的计算是异步的如果 CPU 端立即读结果拿到的几乎一定是上次遗留的数据。正确的同步手段有两种一种是调用vkQueueWaitIdle做全局同步简单粗暴但会把 GPU 流水线全部打断性能很差另一种是在 buffer 上插入 barrier配合vkWaitSemaphores做精细同步。推理场景推荐后者因为模型里的多层算子之间也需要中间 barrier提前把同步机制跑熟练后面搭网络时才能事半功倍。3.4 显存与 Buffer 管理的关键细节Mali G720 的内存模型和桌面 GPU 不太一样它的统一内存架构让 host visible 和 device local 的边界比较模糊但 Vulkan 依然要求你按照堆属性来区分内存。在实际开发中我分了三类 buffer输入输出 buffer使用 host visible host coherent方便 CPU 直接读写中间结果 buffer使用 device local完全不经过 CPU权重 buffer使用 device local初始化时通过 staging buffer 拷贝进去。判断一个 buffer 应该用哪类内存不能靠猜必须调用vkGetBufferMemoryRequirements获取VkMemoryRequirements然后遍历内存类型判断是否满足属性位。我踩过一个典型的坑某个 buffer 明明只需要设备端访问但我选内存类型时优先了 host 可见的堆导致后续用vkCmdCopyBuffer在设备端搬移时效率骤降推理时间多了将近一倍。后来强制 device local 之后立竿见影。还有一个细节是 buffer 的 offset 对齐。Vulkan 里大多数 buffer 要求偏移量对齐到 16 到 256 字节VkPhysicalDeviceLimits里的minStorageBufferOffsetAlignment给出了具体值。Mali G720 上这个值是 16 或 32具体以查询为准。如果你的推理输入是一个结构体数组建议先填充成对齐后的布局而不是让 GPU 端去猜 offset否则很容易出现越界读。4. 端侧推理的实际落地4.1 推理框架选型NCNN 还是自研算子把 Vulkan 计算管线跑通之后面临的真正问题是模型推理怎么在 GPU 上高效完成。现成的选择主要是 NCNN 和 MNN两者都有 Vulkan 后端但在这类 RISC-V OpenHarmony 平台上都有各自的适配成本。NCNN 的 Vulkan 后端是在每个算子里内置 GLSL shader运行时通过 Vulkan 提交计算对 Mali 的支持相对成熟社区里也有人拿它跑过麒麟芯片所以兼容性风险最低。但 NCNN 在 OpenHarmony 上编过的人不多有几个官方示例代码强依赖 Android 的 helper需要自己改。SIGSEGV 问题主要出在线程模型上NCNN 默认用线程池管理每个 GPU 队列OpenHarmony 的 pthread 行为虽然和 Linux 基本一致但某些版本上调度策略不同建议把线程数显式设置为 2 到 4别让它自动探测。MNN 的 Vulkan 后端接口更干净缺点是它的 shader 生成模块体积大交叉编译时经常因为工具链不兼容报错。我最终的选择是NCNN 做整网验证和 benchmark自研几个核心算子卷积、矩阵乘、激活替换热点路径这样既能用现成网络跑通流程又能精确控制性能。4.2 模型转换与数据预处理模型的来源通常是 ONNX 或者 PyTorch 导出的权重。在新版 OpenHarmony 上踩过模型转换的坑之后我的建议是先做一轮 ONNX 精简把无用节点和中间计算去掉再用在线工具转成 NCNN 格式。精度上fp16 是必选项虽然 Mali G720 的 fp16 计算速度比 fp32 有明显优势但前提是驱动支持VK_FORMAT_R16G16B16A16_SFLOAT且 GPU 的 shader 里没有把 fp16 降级成 fp32 的隐藏行为这点通过对比 fp16 和 fp32 的推理结果差异就能判断。数据预处理我放在 CPU 端做因为 input tensor 通常比较小CPU 上做 resize 和 normalize 的耗时在可接受范围内。如果你后续要追求极致性能可以考虑把预处理也写成 compute shader把整条链路全部放到 GPU 里。我在实现 NCNN 的 Vulkan 后端时发现它默认也有这类优化路径但触发条件苛刻建议先关掉跑通之后再逐步开启。4.3 性能调优三个必须盯紧的指标跑稳之后我做了三轮性能压测总结出三个最值得关注的指标第一是 GPU 空闲率。通过vkGetQueryPoolResults配合 timestamp query 可以拿到每段 dispatch 的耗时如果一段算子只有 0.2 毫秒而两个算子之间因为 barrier 等待了 0.8 毫秒那么问题一定出在同步太频繁。推理网络里的算子上百个barrier 不可能完全消除但可以把相邻且不冲突的算子合并到同一个 command buffer 的连续 dispatch 里之间不加 barrier。第二是内存带宽利用率。Mali G720 的带宽是有限的权重 buffer 的重复加载是最大瓶颈。我的优化方式是尽量复用中间结果 buffer用内存池统一管理避免每个算子都申请新内存。实测下来把原来每层各自vkAllocateMemory的方案改成预分配池整体推理时间能提升 20% 到 30%。第三是 queue 提交频率。如果每个算子都单独vkQueueSubmit一次驱动层开销会非常可观。正确做法是整张网只提交一次中间需要的 barrier 全部编码在同一条 command buffer 里。这一点很多人忽略但也是性能差距最大的一个点。5. 常见问题与排查技巧实录5.1 问题速查表整个开发过程里踩过的坑不少我把典型问题整理成了一张速查表这些问题在 OpenHarmony 移植类似 Vulkan 推理项目的过程中大概率都会遇到。现象可能原因排查思路与解决vkCreateShaderModule报错glslang 生成的 SPIR-V 版本过高加--target-env vulkan1.0重新生成枚举不到物理设备ICD json 路径未注册搜索系统目录里的 json检查 library_path推理结果偶发错误缺少 buffer barrier在读写之间插入vkCmdPipelineBarrierGPU 推理比 CPU 还慢频繁vkQueueSubmit改为单次提交整网合并 dispatch资源泄漏导致运行久了卡顿没有复用 pool预分配 command pool 和内存池交叉编译加载 so 报 undefined symbol工具链不匹配统一使用 SDK 的 clang 与 sysroot5.2 印象最深的三个坑第一个坑是 validation layers 在 OpenHarmony 上根本不加载。开发阶段我在 host 机器上写得很快上板之后 logic 出问题就只能靠 logcat 硬啃。后来发现 OpenHarmony 的 vulkan loader 对 validation layers 的扫描路径和 Linux 不一样系统没打包对应的 layer json。解决办法是自己在代码里手动加载VK_LAYER_KHRONOS_validation的 so或者干脆把pCreateInfo里的 layer 列表留空改用 libspirv 做 shader 校验日常排查用 log 打点定位。第二个坑是 Mali G720 驱动对VK_KHR_shader_float16_int8的支持度。跑量化模型时需要 fp16 存储但有些 shader 里用了 fp16 的数学函数驱动居然返回了VK_ERROR_UNSUPPORTED。这个不是系统级问题是驱动里某个扩展声明了但实际能力不完整。我的解决办法是准备两套 shader一套 fp16、一套 fp32运行时根据驱动能力动态切换。第三个坑比较隐蔽同步vkQueueSubmit返回很快但实际 GPU 执行在若干毫秒之后如果在这个窗口里修改了刚提交的 buffer 内容推理结果就会错乱。在调试时特别容易踩因为本地小规模数据跑得飞快问题很难复现等真数据上来就频繁出错。最终方案是在 CPU 侧维护一个简单的 in-flight 队列每次提交后把资源“借”给 GPU等 semaphore 信号回来之后再归还内存池。最后说几句实话做完这个项目之后我对“端侧推理”这四个字的理解比之前深了不少。很多人觉得硬件的算力决定一切实际把 GPU 真正用起来软件层面有一堆要做的事光是一个 buffer 布局就可能带来成倍的性能差距。如果你也是刚开始在 OpenHarmony 上折腾 Vulkan 推理我的建议很朴素先跑通最小链路再上真实模型最后才碰性能优化顺序不能乱。另外这两天发现的一个小技巧也分享给大家猎豹浏览器都被关了记笔记可以用memos这类本地仓库把每次遇到的问题和修法记下来回头看真是个宝藏。总之这条路能走通前面的大坑我都替你踩过一遍了照着抄作业至少能省下一周的排查时间。后续我还会把 NCNN Vulkan 后端在 RISC-V 上的适配补丁整理一版放出来如果你也卡在同一类问题上欢迎拿着截图来找我聊。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑