早先的几个月, 参与了模型转ONNX的那个工作(即 : drcut), 其主要的目标呢, 是对一些模型予以从到ONNX转换的支持。这几个月就算没有做出什么成果, 不过依旧踩了大量的坑, 在这儿记录下来, 期望能够帮助其他的人。
此地所呈现的乃是首个部分, 即理论篇章, 着重阐释了那些与代码并无关联的某些宏观层面的问题。紧接着往后, 我将会特意撰写一篇实战的篇章, 针对其中所含有的一些具体代码予以剖析, 进而阐明在转化ONNX进程里的一些代码方面的技巧以及需要留意的事项。
(1)转ONNX的意义
一般来讲, 转ONNX仅仅是一种手段, 在后续获取到ONNX模型之后, 还得再次对其实施转换, 举例而言, 转换至达成部署的状态, 或者有些人还要额外增添一个步骤, 先从ONNX转换至caffe, 接着再从caffe进行转移, 缘故在于Caffe相对更为友好, 这里所说的关于友好的界定后续会谈及。
所以, 在转ONNX这项工作开始进行之前, 首先得明确目标后端。ONNX仅仅是一种格式, 跟json没什么两样。只要你符合一定的规则,便都算得上是合法的, 故而仅仅从转成一个ONNX文件来讲是很容易的。然而不同后端设备所接受的onnx并不一样, 所以这才是问题所在的出处。
自带的torch.onnx转换所得的ONNX, 所需的ONNX, 所亟需的ONNX皆不一样。
这里面举一个最简单的的例:
将其视为逆函数的运算, 我们首先来瞅一瞅一个实例, 假定目前存在像这样一个C*H*W的(形状), 这里面的每行二维矩阵皆是相同的, 具体呈现如下。
在这般情形下, 要是我们针对它予以调用, 且调用内容为(=2, =1, pad为0)。
那么会得到两个输出,第一个输出是之后的值:
还有一个是Idx, 也就是每个输出所对应的原本的那个输入, 如此一来, 在进行反向传播操作的时候, 便能够直接将输出的梯度传递给与之对应的输入:
细心的同学会发现其实的Idx还可以有另一种写法:
就是把每一个的idx放置到一块儿, 并非每一个都独自从0起始。这两种书写方式都没毛病, 毕竟只要在反向传播之际保持一致就行。
可是当处于我予以支持的状况下, 就会关联到, 也就是的逆向运算: 把输入与和的输出进行操作, 从而获取到相应的输入。
其实现方式是接收每一个皆起始于0的Idx格式, 然而那种情况却是相反的。所以倘若你期望达成如同那样的运算结果, 那么必须要针对所输入的Idx(也就是与之等效的输入情形)进行额外的处置才行。换句话讲, 转换得出的神经网络图与所需的神经网络图并非一致。
(2)ONNX与Caffe
存在着两种主流的模型部署路径, 将其作为示例来说明, 其中之一是走向 ONNX 之后再继续, 另外一种是朝着 Caffe 之后再延续。个人持有这样的观点, 即当下后者显得更为成熟, 这主要是由 ONNX、Caffe 以及的多种相关性质共同致使其这般结果而生的。

上面所展示的那张表, 罗列出了ONNX与Caffe的若干个不同之处, 其中最为关键的差异点便是op的粒度。比如说, 要是针对Bert的层来进行转换操作, ONNX会将其转变为Scale的组合形式, 然而Caffe也许会单刀直入地生成一个名为Multi-Head的层, 并且向CUDA工程师传达这样的指令: "你得去为我编写一个庞大的'(满心怀疑发展到最终是否会把所有内容都整合成一个层)"。

所以要是有那么一天, 来了一个研究者, 提出了一项全新的处于前沿水平的操作对象, 极有可能它能够立马被转变为ONNX格式(假设这个操作对象所在的实现全部都是依靠Aten库的拼接达成的), 然而对于从事Caffe工作 的工程师而言, 就要再次进行撰写。
好处在于细粒度op特别灵活之意, 坏处在于其运行速度会相对较慢。在过去几年间, 存在着诸多致力于op的工作(像将卷积及其后续跟着的relu合并起来一同计算这种情况), XLA及TVM均有大量工作投入至op领域, 也就是把小型op拼接组合成大型op。
推出的是部署框架, 首要考量的是自然性能, 所以他们的layer粒度都挺粗。正是处于此种情形下, Caffe转换过去具备天然优势。
除此以外,粗粒度同样能够应对分支涌现的问题。在其心目中所认知到的神经网络, 实际上是一个极为单纯的直接有向无环图, 也就是, 在给予固定形状的输入条件下, 开展相同的运算操作, 最终获取到具有固定形状的输出结果。
**目前的一个发展方向是支持 shape,但是还很不成熟。
tensor i = funcA();
if(i==0)
j = funcB(i);
else
j = funcC(i);
funcD(j);
针对上面的网络, 假定funcA、funcB、funcC以及funcD均是onnx所支持的细粒度算子, 那么ONNX便会遭遇一个难题, 其转换得出的DAG, 要么呈现为funcA->funcB->funcD这般的形态, 要么呈现为funcA->funcC->funcD这样的形态, 然而不管是哪一种, 肯定都是存在问题的。
而Caffe可以用粗粒度绕开这个问题
tensor i = funcA();
coarse_func(tensor i) {
if(i==0) return funcB(i);
else return funcC(i);
}
funcD(coarse_func(i))
因此它得到的DAG是:funcA->->funcD
当然了, Caffe所带来的代价便是, 苦逼的HPC工程师需要亲手去写一个......(期望Deep能够尽早解放HPC工程师)。
(3)本身的局限
各位熟悉深度学习框架的同学都清楚, 它能够在已然占据了主流态势的情形下突然出现, 进而成功地抢占了相当大的份额, 其最主要的缘由在于它具备着灵活性。打个不太恰当的比方, 它如同C++, 然而就是这样。
在运行整个神经网络之前, 会先进行一次编译, 从而生成一个称之为DAG(也即有向无环图)的东西, 之后才去运行表示这张图。与之相反的是, 属于边运行边试探以获得进一步运行方向, 此过程是直到运行至该节点并计算得出相应结果, 才能够明确知晓一下个节点具体需计算的内容是什么。
实际上, ONNX是要将处于上层的深度学习框架里的网络模型, 转变成为一张图, 鉴于它自身本来就存在一张图, 所以实际上仅仅需要把原本就有的这张图实实在在地取得并且在原本存在的这张图上进行些许修整便可搞定。
但鉴于根本完全没有任何关于图的这类概念, 所以要是打算去完成朝着ONNX的转向变换, 那就得让ONNX在那边拿个小本子,接着跑上一回,跑到啥样就把啥样记录下来, 再把所记录的结果提炼抽象成犹如一张包含各种情形类物质东西的图。故而进行转ONNX存在在两个天生形成的限制条件。
-
所转换得出的结果仅仅针对特定的输入, 要是更换了一个输入致使网络结构出现了变化, ONNX是没办法察觉到的, (最为常见的情形是要是网络当中存在if语句, 此次的输入走了if的话, ONNX就只会生成与if相对应的图, 将else里面所有的信息都丢弃掉)。
-
得需要较多的计算量, 原因在于得确确实实地去实际运行一回神经网络。
关于以上存在的两个局限情况, 我的本科毕设论文有采用一种办法, 该办法是什么呢, 是经由编译器之内的词法分析, 还有语法分析, 直接去扫描或者的源代码获取某个结构以图相待这样的方式, 通过这样的方式能够在不耗费过多资源的条件下完成模型去ONNX的转换工作, 与此同时也能够得到分支判断等等信息, 此刻放置一个链接(。
),希望大家多多支持
当下, 官方期望借助用以解决分支语句问题的方式, 然而, 照我知晓的状况来看, 其尚算不上十分成熟。