作者 | satanwoo 来源 | http://satanwoo.github.io/
相信从事移动开发的朋友们肯定看到过一个表情包:“iOS 开发没人要啦”。
虽说是搞笑之图,却也反映了移动开发领域的部分焦虑感。网上甚至有文章贴出“难上加难”的数据,称:“相比于 2017 年,2018 年 Android 程序员人均面邀数减少 40%,iOS 程序员降幅更高达 57%,即平均每个移动端程序员在找工作时收到的面邀数比去年减少一半。”
撇开玩笑之言,移动开发人员的焦虑感来自何处?我从自身角度及与他人沟通,大致归纳出如下几点:
• 跨平台框架、如 Flutter 对 Native 研发模式的冲击。
• 业界关注重点从移动时代向人工智能等领域转移。
• 对自身掌握技术壁垒的担忧。
细细品味这三点,我想开发者在面临业界趋势转移,担忧自身竞争力不足才是焦虑产生的内在根本。我曾和几个国内知名的 iOS 开发者闲聊,他们表示:都 9102 年了,从大量公开的文章来看,大家还是局限于研究 Runtime,Runloop,block 源码分析等一些比较缺少创新的知识点,让人感受行业的停滞不前。
当然,也有不少开发者在积极拥抱新技术。身边的许多朋友也在了解机器学习,自学相关课程等。但是其中大部分都反馈:学完了基础知识,不知道如何应用;也不知道这些东西能对自己日常工作带来怎样的帮助。最终的结果就演变成了学了就忘,无法产生实质价值。
那是不是事情就此陷入了僵局呢?抱着怀疑及学习的态度,我在2018年中旬加入了手淘-端智能组,参与了一款名叫 MNN 的深度推理引擎的研发工作。这一年多的开发过程,让我对加深了对机器学习 / 深度学习的理解。但更重要的是,这一年多的亲身经历,让我对过去的观点产生了颠覆式的看法。
在这里,我并不想探讨如何学习机器学习,因为这样的文章数量已经浩瀚如海;相反地,我希望通过这篇文章,阐述在开发推理引擎 MNN1 的过程中,我的思考与收获;希望给许多曾和我一样迷茫的移动开发者,一些亲历的感受和信心。
节约篇幅直接贴出 MNN 的 Github 地址:https://github.com/alibaba/MNN
相信有不少同学都曾和我一样,在了解机器学习的初期被诸多的公式推导所吓退,担心这是一个充斥着算法、数学、理论证明的技术领域。
这个观点没错,如果你想要设计出经典的 MobileNet、ResNet 这样的深度神经网络或者是对 Yolo 这样的结构进行复杂度优化,如 Yolo V3 等,你势必要对数学证明、算法优化等方面有较深刻的理解,从这个角度看,说一句很残酷的话:移动工程师跨界的机会不大。
但是机器学习是不是只有算法?这个观点是偏颇的,机器学习本质上是一个工程开发、算法优化与实际应用结合的领域。
用 Caffe 框架之父 / Tensorflow 核心开发者 贾扬清 的观点来看:AI 是一个系统工程,90%的工作在算法之外。
换句话说,机器学习还包含系统工程这个范畴。往小了说,模型可视化工具、转换工具;往大了讲,学术界探索机器学习的编译优化系统,比如陈天奇提出的 TVM 等等,这都是机器学习的一部分。
因此,对于我们移动开发者来说,我们更适合从系统工程的角度,通过实际编程解决问题,去探索机器学习。
备注:这个观点并不是我自己想象出来。大家可以看看机器学习泰斗级人物 Jeff Dean 和李飞飞等人在2017年发表的机器学习系统白皮书。SysML: The New Frontier of Machine Learning Systems2
如同大家学习编程时听过的那样,算法和数据结构是核心能力,一通百通。那么从系统工程的角度来看,无论是机器学习抑或是移动开发,存在诸多共通点是可以相互借鉴。限于篇幅,我仅仅列举几点能够切实帮助我自身日常开发的:
曾有人戏言“移动开发就是 UITableView + JSON”。虽然是句玩笑话,但也能看出数据传输在移动开发中的重要性。从个人经验来看,绝大多数的移动端数据传输协议基本都采用了 JSON(可能部分公司设计了自己的数据协议)。但是 JSON 存在几个缺点(不考虑优化的前提):
• 不内存友好,相对会带来性能瓶颈。
• 需要人为的解析流程。
• 不具备很好的类型解释性。
为了解决类似的问题,一些新的数据协议,如 FlatBuffer 也渐渐进入大家的视线之中。尽管之前就对其有所耳闻,但是真的深入了解还是要追溯到开发推理引擎的过程中。在设计机器学习模型存储结构中,大名鼎鼎的 TFLite,MNN 等框架都采用了 FlatBuffer,这是一种具备 Access to serialized data without parsing/unpacking
的存储结构。它不仅减少了模型的存储大小、提升了性能,也对模型结构扩展、解析自描述起到了巨大的帮助。
尤其是协议自解析方面,真是令我大开眼界。简单来说,你只要按照 FlatBuffer Schema 要求的方式定义你的数据结构,剩下的编码 / 解析的过程都自动化完成。
这里以 MNN 框架中的 FlatBuffer 的使用举例,比如整个神经网络的拓扑架构定义如下:
table Net {
bizCode: string;
extraTensorDescribe: [TensorDescribe];
gpulibrary: GpuLibrary;
oplists: [Op];
outputName: [string];
preferForwardType: ForwardType = CPU;
sourceType: NetSource = CAFFE;
tensorName: [string];
tensorNumber: int = 0;}
整体 MNN 中 Schema 的设计可以参考:https://github.com/alibaba/MNN/tree/master/schema/default
然后我们通过一行简单的命令(这里仅作演示举例)就可以自动生成 JavaScript 的对应代码。
./flatc -s -I ~/MNN/schema/default ~/MNN/schema/default/MNN.fbs
/**
* @constructor
*/
MNN.Net = function() {
/**
* @type {flatbuffers.ByteBuffer}
*/
this.bb = null;
/**
* @type {number}
*/
this.bb_pos = 0;
};
/**
* @param {number} i
* @param {flatbuffers.ByteBuffer} bb
* @returns {MNN.Net}
*/
MNN.Net.prototype.__init = function(i, bb) {
this.bb_pos = i;
this.bb = bb;
return this;
};
/**
* @param {flatbuffers.ByteBuffer} bb
* @param {MNN.Net=} obj
* @returns {MNN.Net}
*/
MNN.Net.getRootAsNet = function(bb, obj) {
return (obj || new MNN.Net).__init(bb.readInt32(bb.position()) + bb.position(), bb);
};
/**
* @param {flatbuffers.ByteBuffer} bb
* @param {MNN.Net=} obj
* @returns {MNN.Net}
*/
MNN.Net.getSizePrefixedRootAsNet = function(bb, obj) {
return (obj || new MNN.Net).__init(bb.readInt32(bb.position()) + bb.position(), bb);
};
/**
* @param {number} index
* @param {MNN.TensorDescribe=} obj
* @returns {MNN.TensorDescribe}
*/
MNN.Net.prototype.extraTensorDescribe = function(index, obj) {
var offset = this.bb.__offset(this.bb_pos, 6);
return offset ? (obj || new MNN.TensorDescribe).__init(this.bb.__indirect(this.bb.__vector(this.bb_pos + offset) + index * 4), this.bb) : null;
};
/**
* @returns {number}
*/
MNN.Net.prototype.extraTensorDescribeLength = function() {
var offset = this.bb.__offset(this.bb_pos, 6);
return offset ? this.bb.__vector_len(this.bb_pos + offset) : 0;
};
/**
* @param {number} index
* @param {MNN.Op=} obj
* @returns {MNN.Op}
*/
MNN.Net.prototype.oplists = function(index, obj) {
var offset = this.bb.__offset(this.bb_pos, 10);
return offset ? (obj || new MNN.Op).__init(this.bb.__indirect(this.bb.__vector(this.bb_pos + offset) + index * 4), this.bb) : null;
};
而用户在代码中使用这个拓扑结构,只要简单调用入口函数 getRootAsNet ,剩下来的一切都自动化完成。而当你要修改结构定义的时候,仅仅需要修改对应的 Schema 文件,重新生成对应的解析文件,无需人工逐字段手工修改。
限于篇幅有限,这里不过多展开对 FlatBuffer 的介绍,感兴趣的读者可以阅读 MNN 用户自发写的博客《FlatBuffers,MNN模型存储结构基础 ---- 无法解读MNN模型文件的秘密3》。
那这样的协议能不能应用于移动开发中并起到正向的作用呢?答案是肯定的,有兴趣的朋友可以阅读 Facebook 的相关文章。
部分读者可能知道,我和几位同事在知乎上开了一个专栏《iOS调试进阶》4,重点分享 ARM 相关的汇编知识。会有这个想法是因为日常工作中排查许多 Crash 的时候,从源码层面已经无法定位,必须要依赖计算机执行的本质 - 机器码进行分析,而这正是汇编可以产生价值的地方。
但是汇编不仅仅局限于排查 Crash。在开发 MNN 过程中,涉及了大量的密集型计算操作。团队的一些大牛在指令实现层面根据流水线编排、硬件大小核数、缓存大小等等,使用手写汇编来精细化调度数据的读写与执行,使得MNN 的推理性能达到了业界一流的水准(无论是我们自己的 benchmark 抑或是利益无关的友商的评测都证明了这一点)。而阅读这些精心酿造的汇编代码,会让你感到,原来开发还能这么玩!
这里展示一个经典的 Bilinear 插值通过汇编的实现:
text
.align 5
asm_function MNNBilinearProcC1
//void MNNBilinearProcC1(const unsigned char *sample, unsigned char* dst, const int16_t* xFactor, const int16_t* yFactor, size_t w);
//Auto: x0:sample, x1:dst, x2:xFactor, x3:yFactor, x4:w
ld1 {v31.s}[0], [x3]
//Now x3 is no used
dup v30.4h, v31.h[0]
dup v31.4h, v31.h[1]
L8:
cmp x4, #8
blt End
LoopL8:
ld4 {v4.8b, v5.8b, v6.8b, v7.8b}, [x0], #32
ld2 {v0.8h, v1.8h}, [x2], #32//q0, q1
//(x00,x01) -> (y0)
uxtl v2.8h, v4.8b
uxtl v3.8h, v5.8b
umull v16.4s, v2.4h, v0.4h
umull2 v17.4s, v2.8h, v0.8h
umlal v16.4s, v3.4h, v1.4h
umlal2 v17.4s, v3.8h, v1.8h
uqshrn v18.4h, v16.4s, #4
uqshrn v19.4h, v17.4s, #4
//(x10,x11) -> (y1)
uxtl v2.8h, v6.8b
uxtl v3.8h, v7.8b
umull v16.4s, v2.4h, v0.4h
umull2 v17.4s, v2.8h, v0.8h
umlal v16.4s, v3.4h, v1.4h
umlal2 v17.4s, v3.8h, v1.8h
uqshrn v20.4h, v16.4s, #4
uqshrn v21.4h, v17.4s, #4
//(y0,y1) -> dst
umull v16.4s, v18.4h, v30.4h
umull v17.4s, v19.4h, v30.4h
umlal v16.4s, v20.4h, v31.4h
umlal v17.4s, v21.4h, v31.4h
uqshrn v2.4h, v16.4s, #16
uqshrn2 v2.8h, v17.4s, #16
uqrshrn v0.8b, v2.8h, #2
st1 {v0.8b}, [x1], #8
sub x4, x4, #8
cmp x4, #8
bge LoopL8
End:
相信我,当你从不懂汇编 -> 读懂汇编 -> 手写汇编,每前进一步,你会发现更广阔的天地。有一天当你要做性能优化,发现许多网上常见的手段都使用过了但仍然不起作用的时候,也许汇编就是你杀手锏。
近些年来随着短视频的崛起,市面上渲染、多媒体相关的岗位也越加变得火热。而这些岗位无一例外都需要对 GPU 有着深度的了解。而操作 GPU,自然而然就少不了与 Shader 打交道。
Shader 其实就是专门用来渲染图形的一种技术。通过 Shader ,我们可以自定义显卡渲染画面的算法,使画面达到我们想要的效果。
但 Shader 的作用不仅仅作用于渲染。在机器学习领域,苹果的 Metal 框架所包含的Metal Performance Shader(MPS)也能用来做 GPU 计算,提升机器学习在移动端的执行性能。就连诞生已久的 OpenGL,也在最新的 OpenGL 3 标准中增加了计算纹理,支持 GPU 计算的能力。由此可见,尽管最初的目的并不相同,但是技术本质是相通的,最后都会产生微妙的化学反应。
上述几点,仅仅是个人抛砖引玉,展示机器学习和日常移动开发相互交织的冰山一角。从工程实现的角度,仍有许多值得探索并实践应用的,欢迎大家一起探讨交流。
读到这,可能有些读者内心的兴奋之情被熊熊点燃,恨不得立刻能将相关的知识学习起来;但也有部分朋友会觉得,可能只有 BAT 这样的大厂才会有实际的场景需要进行如此深入的研究和开发工作,有沮丧之情。
我对这种体会特别感同身受,因为去年刚转型开发 MNN之初,我也有过手足无促,连简单的 Metal Performance Shader 都写不好。加上之前有些朋友通过 QCon 和云栖大会听闻了 MNN,也和我或其他同事进行过一些实现上或者应用方面的探讨。
因此,除了希望通过这篇文章带领大家对机器学习系统有一个全新的认知之外,后续也会以连载的方式,在以下两个方面给大家继续带来更多有价值的点:
• 技术介绍,我会把 MNN里面使用的相关技术点,逐个拆解,带领大家通过理论探索和实际编程相结合的方式来深入了解细节,反哺于大家日常的开发工作。
• 最佳实践,目前在客户端领域应用机器学习的典型案例还比较缺乏。而我正好在过去一年多的时间里,探索了诸多的实践案例(比如大家耳熟能详的拍立淘、淘宝直播、AR试妆等等中都有 MNN 的身影哦~),我也会将其整理分享出来,和大家一起探索端智能的前行之路。
本文记录了过去一年多,个人参与 MNN 框架相关开发过程中的一些收获与心得。如何不分裂的看待机器学习与移动开发的关系,如何从看似不相关的领域寻找共同点,提升自己所处领域的价值和核心能力,是值得我们每位开发同学需要思考的。
在最后,还是要说一句:移动客户端的从业人员并不需要过多的焦虑和担忧,动态化、高性能、内核、渲染等等方向都充满前景。但是,你需要找到你所擅长且愿意为之深入的,这才是你保证在浪潮中不被拍翻的核心竞争力。
[1]https://github.com/alibaba/MNN [2]https://arxiv.org/abs/1904.03257 [3]https://www.jianshu.com/p/8eb153c12a4b [4]https://zhuanlan.zhihu.com/c_142064221