算子基础与计算图原理
各位同学,今天我们来聊聊算子与计算图。说实话,这部分内容看起来偏理论,但如果你真想做好YOLO的算子适配,这些概念绕不过去。我当年刚入行时,就是在这上面栽过跟头——以为只要会调库就行,结果一碰到自定义算子就抓瞎。
什么是算子?
算子,英文叫Operator,简称Op。说白了,它就是神经网络里一个最基本的计算单元。你可以把它想象成乐高积木里的一个基础块——卷积是一个块,ReLU是一个块,加法也是一个块。
我个人习惯把算子分成三类:
- 数学运算类:加减乘除、指数、对数等
- 张量操作类:reshape、transpose、concat等
- 神经网络专用类:卷积、池化、归一化等
张量计算图
计算图,就是把这些算子串起来的那个"图"。每个节点是一个算子,每条边是张量数据流。你想想看,YOLO从输入图片到输出检测框,中间经过了几十个算子,它们怎么组织?就是靠计算图。
我举个例子,一个简单的计算图长这样:
输入张量 → Conv2d → BatchNorm → ReLU → MaxPool → 输出张量
[Op1] [Op2] [Op3] [Op4]
每个箭头传递的是张量(Tensor),每个方框是算子。就这么简单。
关键点:计算图是静态的还是动态的,决定了你适配算子的方式。PyTorch用的是动态图,TensorFlow 2.x也是动态的,但ONNX导出的是静态图。做算子适配时,我建议你优先理解静态图——因为部署场景下,静态图才是主流。
前向推理与反向传播
这两个概念,我分开讲。
前向推理:输入数据从计算图的起点一路跑到终点,算出结果。在YOLO里,就是输入一张图片,输出三个尺度的特征图,再解码成检测框。这个过程只涉及算子的正向计算。
反向传播:训练时才有的过程。从损失函数开始,沿着计算图反向走,计算每个算子的梯度。这需要每个算子都实现一个反向函数。
做算子适配时,你只需要关心前向推理。为什么?因为部署场景下,模型已经训练好了,我们只做推理,不做训练。所以反向传播的适配,基本不用管。
算子粒度划分
算子粒度,就是算子的大小。我把它分成三个级别:
| 粒度类型 | 特点 | YOLO中的例子 | 适配难度 |
|---|---|---|---|
| 逐元素 | 输出每个元素只依赖输入对应位置 | ReLU、Sigmoid、加法 | 低 |
| 归约 | 输出元素依赖输入多个元素,维度降低 | 全局平均池化、Softmax | 中 |
| 卷积 | 输出元素依赖输入局部区域,有滑动窗口 | Conv2d、DepthwiseConv | 高 |
逐元素算子是最简单的。每个输出只跟一个输入元素有关,没有数据依赖。适配时基本就是写个循环,或者直接映射到硬件的SIMD指令。我在适配YOLOv5的SiLU激活函数时,就是逐元素的,一个晚上就搞定了。
归约算子稍微复杂一点。它需要把多个输入元素合并成一个。比如全局平均池化,要把整个特征图的所有值加起来再除以总数。这里要注意的是数据精度——累加过程中可能会溢出。我曾经在适配Softmax时,因为中间结果太大,导致精度丢失,检测结果全乱了。
卷积算子是最难的。它涉及滑动窗口、多通道累加、步长、填充等概念。YOLO里最核心的就是卷积,尤其是3×3卷积和1×1卷积。适配卷积时,我建议你重点关注内存访问模式——数据重排、分块策略、DMA传输,这些才是性能的关键。
核心原则:算子粒度越细,灵活性越高,但性能越差。反之,粒度越粗,性能越好,但灵活性越低。做算子适配时,我一般先保证功能正确,再考虑性能优化。别一上来就想着怎么把算子融合——那是后面的事。
知识体系总览
下面这张图,把本章的核心内容串起来了。你可以把它当作一个思维导图来看:
这张图把本章的核心概念都串起来了。从算子定义出发,到计算图的结构,再到前向推理和反向传播的区别,最后落到算子粒度划分。你可以看到,卷积算子处于最复杂的位置——它既是神经网络专用类,又是粒度最粗的算子。这也是为什么YOLO算子适配的难点,往往都集中在卷积上。