如何为 Halide 后端调度网络
对于我们所使用的每一种设备,Halide 代码都是相同的。但要达到令人满意的效率,我们必须正确地调度计算。在本教程中,我们将介绍在 OpenCV 深度学习模块中使用 Halide 后端调度网络的若干方法。
为了更好地理解 Halide 的调度,你可能需要阅读 http://halide-lang.org/tutorials 上的教程。
如果你是第一次在 OpenCV 中接触 Halide,我们建议先从 tutorial_dnn_halide 开始。
你可以通过编写文本配置文件来调度 Halide pipeline 的计算。这意味着你可以轻松地对层计算的循环进行向量化、并行化以及调整顺序。在第一次调用 cv::dnn::Net::forward 之前,将包含特定设备调度指令的文件路径传入 cv::dnn::Net::setHalideScheduler。
调度配置文件以 YAML 文件的形式表示,其中每个节点都是一个被调度的函数或一条调度指令。
relu1: reorder: [x, c, y] split: { y: 2, c: 8 } parallel: [yo, co] unroll: yi vectorize: { x: 4 }conv1_constant_exterior: compute_at: { relu1: yi }约定使用变量 n 表示 batch 维度,c 表示通道,
y 表示行,x 表示列。对于 split 之后的变量,使用带有相同前缀但分别带有 o 和 i 后缀的名称来表示外层和内层变量。例如,对于范围在 [0, 10) 内的变量 x,指令
split: { x: 2 } 会生成范围在 [0, 5) 内的 xo 和范围在 [0, 2) 内的 xi。
变量名 x 在同一个调度节点中将不再可用。
你可以在 opencv_extra/testdata/dnn 中找到调度示例,并用它们来调度你的网络。
得益于层融合,我们只需调度融合集合的最顶层。因为对于每一个输出值,我们使用的都是融合后的公式。 例如,如果你依次有三个层 卷积(Convolution) + 缩放(Scale) + ReLU,
conv(x, y, c, n) = sum(...) + bias(c);scale(x, y, c, n) = conv(x, y, c, n) * weights(c);relu(x, y, c, n) = max(scale(x, y, c, n), 0);融合后的函数类似于
relu(x, y, c, n) = max((sum(...) + bias(c)) * weights(c), 0);因此,只有名为 relu 的函数需要调度。
有时网络采用分块结构构建,这意味着某些层是完全相同或非常相似的。如果你想对不同的层应用相同的调度(仅 tiling 或向量化因子有所不同),可以在调度文件开头的 patterns 部分定义调度模式。
此外,你的模式还可以使用一些参数化变量。
# 在文件开头patterns: fully_connected: split: { c: c_split } fuse: { src: [x, y, co], dst: block } parallel: block vectorize: { ci: c_split }# 在下方的某处fc8: pattern: fully_connected params: { c_split: 8 }你可以让 DNN 自动调度各层。只需跳过对 cv::dnn::Net::setHalideScheduler 的调用即可。有时它甚至可能比手动调度更高效。
但如果某些特定层需要手动调度,你可以混合使用手动和自动两种调度方式。编写调度文件,
并跳过那些你希望自动调度的层。