Skip to content

如何为 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 的调用即可。有时它甚至可能比手动调度更高效。 但如果某些特定层需要手动调度,你可以混合使用手动和自动两种调度方式。编写调度文件, 并跳过那些你希望自动调度的层。