作者:nihui
转自:知乎
为什么要有 spirv
先讲点历史。在使用 opengl 的年代,创建 pipeline,是直接把glsl 代码传给 opengl 函数,由 gpu 驱动收到后负责将 glsl 代码编译为底层的指令。 随着 opengl 功能越来越多,glsl 有了许多扩展语法。然而 gpu 驱动质量不一,有些驱动过于老旧,遇到新的 glsl 扩展代码会导致出错或者崩溃。 为了进一步打薄 gpu 驱动,vulkan 使用了机器解析友好的二进制格式 spirv,开发者将 spirv 二进制传给 vulkan 函数,spirv 格式定义非常简单,扩展性和兼容性比 glsl 代码优秀得多,驱动可以很容易的处理 spirv 并兼容未来可能的扩展。此外,编译时 glsl 转换为 spirv,可以更早的发现代码的语法错误。
vulkan 的清真用法
vulkan 1.0 最初版只接受 spirv 代码,即 VkShaderModuleCreateInfo pCode 必须是 spirv 数据的地址。spirv 则使用 glslangValidator,从 glsl 代码转换得到。看起来还行,用些 cmake 的技巧,编译时自动生成 spirv,就能自动化这个步骤。也许 nvidia 觉得那些 opengl 程序员一时半会接受不了,很早就推出了 VK\_NV\_glsl\_shader 扩展。开启这个扩展后,允许用 glsl 代码创建 shadermodule,和 opengl 非常相似的用法。
ncnn 使用 vulkan 1.0
为了最好的兼容性,ncnn 使用 vulkan 1.0 标准和扩展。这样的优点是所有支持 vulkan 的 gpu 都可以正常跑起来。vulkan api 设计时充分考虑的前向兼容性质,绝大多数 vulkan 1.1乃至1.2的新扩展,都有对应的 vulkan 1.0版本。其中 VK\_KHR\_16bit\_storage 和 VK\_KHR\_shader\_float16\_int8 是两个针对 gpgpu 计算任务比较重要的扩展,分别加入了 fp16 存储和 fp16/int8 运算的功能。ncnn 运行时会检查设备对扩展的支持情况,并适当开启扩展,充分利用 gpu 的硬件能力加速推理。然而,这两个扩展也对 glsl 做了语法层面的补充,无法利用 vulkan specialization constant 在编译 pipeline 时依据扩展支持情况设置开关。如此一来,只能在离线编译时,预先生成 fp32 和 fp16 两个版本的 spirv 二进制,而后在创建 shadermodule 时在多个版本 spirv 切换。这样的做法可以完美解决 ncnn 的需求,即在不支持 fp16 的 gpu 上跑 fp32 的 shader 代码,在支持 fp16 的 gpu 上跑 fp16 的 shader 代码。
ncnn vulkan 库体积爆炸
硬件多起来了,扩展多起来了。除了上述的 fp16 存储和 fp16 运算,ncnn 为不支持 fp16 存储的 gpu 实现了 fp16 packed 的软存储实现,通过 packHalf2x16 和 unpackHalf2x16 也可以大幅减少 gpu 显存带宽的需求,获得明显的加速。手机上的高通芯片内置 adreno gpu,这种 gpu 只有使用 image 数据类型时才能获得更快的数据访问,而 ARM Mali gpu 则偏好 buffer 数据类型。ncnn 主要是面向移动端优化的框架,因此也需要加入 image 存储的 spirv 版本。至此,同一份 ncnn shader 代码会离线生成 fp32/fp16p/fp16s/fp16a/image-fp32/image-fp16p/image-fp16s/image-fp16a 10种不同版本的 spirv 二进制,这些二进制都会打包到 ncnn vulkan 库里面,库体积一下子就爆炸式增长到了几十M,放在 app 安装包里实在不能忍。
运行时编译 spirv
原始的 ncnn shader 代码并不大,如果直接能用 glsl 创建 shadermodule 就可以避免打包10个 spirv。可惜 vulkan 1.1 用不得,VK\_NV\_glsl\_shader 也基本是 nvidia 独占,手机上是没有的,唯一可行的只能由 ncnn 完成运行时 glsl 到 spirv 的转换。说实话,我不喜欢第三方库,但增加第三方库反而能大幅缩小库体积,还是很不错的。于是,glslang 就这样引入了,效果也很不错,ncnn 库从之前的几十M降到了几M。前几天我发了个 ncnn-android-benchmark 就是用这种方式编译打包的,armv7+armv8 两个架构,支持 CPU/GPU,不经过任何裁剪的 ncnn 库,apk 只有 3.4M,推理速度和离线编译 spirv 没有区别。
一些备注
- ncnn 新增了 NCNN\_VULKAN\_ONLINE\_SPIRV cmake 开关用来控制是否使用运行时编译 spirv,git master 上已设为默认开启
- ncnn 新增了 NCNN\_SYSTEM\_GLSLANG cmake 开关用来控制是否使用系统已有的 glslang 库而非 git submodule 中的版本,默认不开启
- android 编译打包时额外需要链接 libglslang.a libOGLCompiler.a libOSDependent.a libSPIRV.a 这些库
- macos/ios 的 MoltenVk.framework 自带了 glslang 实现,因此不额外提供,和之前一样链接 MoltenVk 就可以进行运行时编译 spirv
推荐阅读
更多嵌入式AI算法部署等请关注极术嵌入式AI专栏。