程序员生成指南
item
item-SKU
一个订单可以包含多个订单项,形成 一对多 关系。
订单项代表订单中所购买的每个商品的详细信息。
1 | package orderserver |
1 | type OrderPayRequest struct { |
1 | package fulfillmentserver |
1 | package refundservice |
1 | // RefundApproveRequest 包含退款审批请求所需的参数 |
1 | // ReturnPurchaseRequest 包含退货请求所需的参数 |
1 | package refundservice |
策略 | 优点 | 缺点 |
---|---|---|
1. 直接读取主库 | - 一致性: 始终获取最新的数据。 | - 性能: 增加主库的负载,可能导致性能瓶颈。 |
- 简单性: 实现简单直接,因为它直接查询可信的源。 | - 可扩展性: 主库可能成为瓶颈,限制系统在高读流量下有效扩展的能力。 | |
2. 使用VersionCache与从库 | - 性能: 分散读取负载到从库,减少主库的压力。 | - 复杂性: 实现更加复杂,需要进行缓存管理并处理潜在的不一致性问题。 |
- 可扩展性: 通过将大部分读取操作卸载到从库,实现更好的扩展性。 | - 缓存管理: 需要进行适当的缓存失效处理和同步,以确保数据的一致性。 | |
- 一致性: 通过比较版本并在必要时回退到主库,提供确保最新数据的机制。 | - 潜在延迟: 从库的数据可能仍然存在不同步的可能性,导致数据更新前有轻微延迟。 |
1 | npm i -g hexo 安装hexo |
1 | deploy: |
最近在项目中使用到visp库的模板追踪算法(template tracker),由于接触算法的时间比较短,这里简单记录对算法的理解和认识。模板追踪算法原理比较简单,当代价函数为SSD时,抽象为数学中的非线性最优化问题,这里采用高斯牛顿法求解。高斯牛顿法应该是通用的一种求最优化问题的算法,高斯牛顿法核心是迭代公式,不断迭代更新出新的参数值。visp模板算法效率本身不高,因此在实现的时候提供了一些可调的优化的参数,例如金字塔、采样率、迭代次数、误差等。在项目中,visp模板追踪算法在参考模板没有遮挡的情况下,效果基本满足要求,但是在有遮挡的情况,会存在比较大的问题,因此我们针对遮挡情况,进行了特别的优化。除此之外,我们优化了一个并行版本的模板追踪算法,提升追踪效率。
在了解visp模板追踪算法之前,可通过官网上的视频了解追踪算法的能力。它和kcf之类的追踪算法还不太一样,在kcf追踪算法中,我们需要告诉追踪器的追踪目标,通常情况下,我们不要求像素级别的进度的要求。而template tracker参考模板(reference template)计算视频中两帧之间的单应矩阵Homography,通过单应矩阵计算目标区域在当前帧的位置,从而实现追踪的效果。
visp库中为模板追踪算法提供了SSD、ZNNC和在VISP 3.0.0时引入的MI(mutual information) 代价函数。这里以SSD代价函数描述模板追踪算法。模板追踪算法在数学描述为一个最优化问题,通过SSD代价函数,缩小误差,寻找最优的标记帧到当前帧的单应矩阵。模板追踪算法的数学描述如下:
$$ H_t = \arg \min \limits_{H}\sum_{x∈ROI}((I^*(x)-I_t(w(x,H)))^2 $$
这里使用经典的高斯牛顿法(Gauss–Newton algorithm)迭代法求解,关于高斯牛顿法这里就不赘述了,最关键的是其迭代公式,感兴趣可以参考下面两篇文章:
其迭代公式如下,$J$表示雅克比矩阵,$J^T$表示$J$的转置,$H_t$表示迭代的初始值,$H_k$表示上一次迭代的结果,$r(H_k)$表示上一次迭代的残差residual。
$$ H_{t+1} = H_t + (J^TJ)^{-1}J^Tr(H_k) $$
了解了模板追踪算法的数学描述和高斯牛顿迭代算法,其基本实现应该是不难的,它本质上是一个迭代算法主要分为以下几步:
step1. 设定初始的$H$矩阵,第一帧为单一矩阵,之后上一帧的结果.
step2. 对于第$k$次迭代计算雅克比$J$, 残差$r(H_k)$,得到$\triangledown H=-(J^TJ)^{-1}J^Tr(H_k)$.
step3. 如果$\triangledown H$ 足够小或者达到最大循环次数就停止迭代
step4. 如果不满足迭代停止条件$H_{k+1}=H_{k} +\triangledown H$
step5. 迭代结束时,$H_{t+1}=H_{k}$
1 | # img0 表示标记帧 |
1 | # for deltaH |
为提高模板追踪算法的效率,visp库在实现模板追踪算法的时候设置了一些可调的参数:
实际使用visp模板追踪算法中,发现当参考模板处有物体遮挡时,效果不好,因此需要做进一步的处理。另外,我们在工程实践时,为了提高追踪的效率,升级了一个并行版本的追踪,能提高数倍的追踪效率。
参考链接:
最近在做kcf算法在移动端优化的相关工作,由于kcf算法计算量太大,而移动端计算性能有限,因此打算将kcf部分耗时操作通过GPU计算进行提升算法的性能。由于接触GPU和OpenCL的时间比较短,原理性的东西理解得也不深刻,本文主要在移动端测试了一些GPU和OpenCL的数据,无法分析内在原因,方便后续移动端算法优化。主要工作如下:
KCF算法总使用了不少的opencv函数,开始想的是编译一个包含OpenCL的opencv的sdk,然后通过调用该sdk从而实现使用GPU加速算法的目的。编译opencv+OpenCL的sdk当时踩了不少坑,多番尝试之后,使用下面的命令是可以成功编译。分别下载opencv-3.4.6、android-ndk-r16b、opencv_contrib-3.4.6,在opencv中建build目录,运行下面命令,命令中使用一些路径相关的参数要根据环境适当修改。
1 | cmake -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON -DCMAKE_TOOLCHAIN_FILE="/home/xxx/code/mobile/third_party/ opencv-3.4.6/platforms/android/android.toolchain.cmake" -DANDROID_NDK="/home/xxx/code/mobile/tools/android-ndk-r16b" -DANDROID_SDK="/home/xxx/code/mobile/tools/android_sdk/tools" -DANDROID_NATIVE_API_LEVEL=19 -DANDROID_ABI="arm64-v8a" -DANDROID_ARM_NEON=TRUE -DANDROID_STL=gnustl_static -DCMAKE_BUILD_TYPE=Release -DOPENCV_EXTRA_MODULES_PATH="/home/xxx/code/mobile/third_party/opencv_contrib-3.4.6/modules" -DCMAKE_INSTALL_PREFIX="/home/xxx/code/mobile/third_party/opencv-3.4.6/install_20190623_OpenCL" -DBUILD_opencv_java=ON -DBUILD_ANDROID_PROJECTS=OFF -DBUILD_ANDROID_EXAMPLES=OFF -DBUILD_DOCS=OFF -DBUILD_PERF_TESTS=OFF -DBUILD_TESTS=OFF -DBUILD_FAT_JAVA_LIB=OFF -DWITH_OpenCL=ON -DWITH_CUDA=OFF -DWITH_MATLAB=OFF -DBUILD_opencv_aruco=OFF -DBUILD_opencv_calib3d=OFF -DBUILD_opencv_features2d=OFF .. |
在编译好opencv sdk之后,首先简单测试了一下sdk是否使用到了GPU资源。测试图片从CPU拷贝到GPU的的性能,opencv提供两组API。UMat::copyTo(OutputArray dst)和Mat::getMat(int access_flags),实际测试中发现copyto那组性能比get的性能更好些,mat.getUmat函数会报错,还不知道什么原因。
1 | void testMatCopyToUmat(const char* img, int times) { |
| 手机型号 | CPU型号 | GPU型号 | OpenCL版本 | 首次mat拷贝umat | mat拷贝umat | 首次umat拷贝mat | umat拷贝mat | 图片格式 | 上行带宽 | 下行带宽 |
| —— | —— | —— | —— | —— | —— | —— | —— | —— | —— | —— | —— | —— | —— | —— |
|三星GALAXY On7|高通 骁龙410 MSM8916| Adreno306| 2| 25.2ms| 0.8ms| 1.5ms| 0.8ms| 720480 159KB| 运行1000次,221M/s| 运行1000次,258M/s|
|三星GALAXY On7|高通 骁龙410 MSM8916| Adreno306| 2| 30.18ms| 2.88ms| 5.5ms| 2.9ms| 19201080 6MB| 运行1000次,2.14G/s| 运行1000次,2.14G/s|
|小米6 MI6| 骁龙 835| 高通 Adreno540| 2|16.602ms| 0.754ms| 2.85ms| 0.795ms| 19201080 6MB| 运行1000次,7.9G/s| 运行1000次,8.06G/s|
|小米6 MI6| 骁龙 835| 高通 Adreno540| 2|17.010ms| 0.332ms| 1ms|0.265ms| 720480 159KB| 运行1000次,632M/S| 运行1000次,898.2M/s|
|小米mix2s| 骁龙 845| 高通 Adreno630| 2|8.7ms| 2.1ms| 6.1ms|0.9ms| 19201080 6MB| 运行1000次,6.6G/S| 运行1000次,6.62G/s|
|小米mix2s| 骁龙 845| 高通 Adreno630| 2|3.3ms| 0.5ms| 2.2ms|0.4ms| 720480 1579KB| 运行1000次,654M/S| 运行1000次,682M/s|
在测试完CPU和GPU内存拷贝的性能之外,之后测试了cvtcolor函数的性能,由于动态加载,OpenCL函数首次加载时特别耗时,大概需要200ms。除此之外,在不同规格的图片上,OpenCL的计算性能大概是cpu的2到3倍。
1 | void cpu(const char* img, int times) { |
测试数据:
手机型号 | cpu/gpu | 图片格式 | 首次运行时间 | 平均时间 |
---|---|---|---|---|
三星GALAXY On7 | cpu | 1920x1080 | 3.2ms | 1.8ms |
三星GALAXY On7 | OpenCL | 1920x1080 | 273ms | 0.6ms |
三星GALAXY On7 | cpu | 720x480 | 1.2ms | 0.62ms |
三星GALAXY On7 | OpenCL | 720x480 | 274ms | 0.25ms |
小米mix2s | cpu | 1920x1080 | 3ms | 1.3ms |
小米mix2s | OpenCL | 1920x1080 | 154ms | 0.36ms |
小米mix2s | cpu | 720x480 | 0.5ms | 0.21ms |
小米mix2s | OpenCL | 720x480 | 80.5ms | 0.09ms |
手机型号 | cpux型号 | GPU型号 | OpenCL版本 | API | 测试数据 |
---|---|---|---|---|---|
小米6 MI6 | 骁龙 835 | 高通 Adreno540 | 2 | gpu内存分配(clCreateBuffer) | 1M 430us,5M 1000us,10M 2000us |
小米6 MI6 | 骁龙 835 | 高通 Adreno540 | 2 | cpu到gpu内存拷贝(writeBuffer) | 1M 105us,5M 400us,10M 700us |
小米6 MI6 | 骁龙 835 | 高通 Adreno540 | 2 | gpu到cpu内存拷贝(ReadBuffer) | 1M 60us,5M 400us,10M 600us |
小米6 MI6 | 骁龙 835 | 高通 Adreno540 | 2 | 核函数编译clBuildProgram | 69682 us |
小米6 MI6 | 骁龙 835 | 高通 Adreno540 | 2 | 创建核对象clCreateKernel | 50us |
小米6 MI6 | 骁龙 835 | 高通 Adreno540 | 2 | 核函数clEnqueueNDRangeKernel | 首次运行5000us,之后大概800us |
在OpenCL编程中,work_item和work_group的设置对程序的性能有较大的影响。这里以内存拷贝为例测试OpenCL中work_item数量与效率的关系。通过一张3840x2160的图片拷贝,分别测试了CPU和GPU内存拷贝的性能,测试了在不同work_item条件下GPU内存拷贝性能的性能。从测试结果来看,不同work_item对opencl的性能有较大的影响。测试结果显示,最开始时work_item数量曾倍数关系,之后会在100ms抖动,最好的情况是work_item数量与bmpsize大小相同。测试机器为小米mix2s。
bmpsize = 3840x2160x3,运行时间13ms
1 | char* out = new char[bmp_size]; |
bmpsize = 3840x2160x3,运行时间3ms
1 | memcpy(out,bmp_data,bmp_size); |
核函数:
1 | __kernel void convert_image(__global const uchar* in, |
关键代码与work_item的设置:
1 | P("thread_count=%d", thread_count); |
work_item数量 | 运行时间 |
---|---|
1 | 2972ms |
2 | 1526ms |
4 | 792ms |
8 | 418ms |
16 | 252ms |
32 | 166ms |
64 | 122ms |
128 | 104ms |
256 | 64ms |
512 | 60ms |
1024 | 92ms |
2048 | 662ms |
4096 | 237ms |
10240 | 180ms |
102400 | 171ms |
248832 | 167ms |
2488320 | 16ms |
24883200 | 15ms |
疑问:当work_item为256或者512是个较好的值,但是不明白为什么2488320和24883200值效果会更好。
在学习和测试OpenCL的过程中,有一个疑问能否使用多个commandqueue来做任务的并行。假设有n个任务,每个任务包含CPU到GPU内存拷贝,核函数执行,和GPU到CPU的内存拷贝。分别测试了使用一个commandqueue和n个commandqueue的性能,测试结果显示多个commandqueue会比使用单个commandqueue性能略好一些,但是差别不大。除此之外,与work_item的设置有关,多个commandqueue可能比单个commandqueue性能性能提升15%。从GPU利用率来说,单个commandqueuGPU曲线呈锯齿形状,而多个commandque呈梯形。部分代码如下:
单个commandqueue:
1 | void test(const char* cl_file, const char* name, |
多个commandqueue:
1 | void test_mutil_command_queue(const char* cl_file, const char* name, |
今年是2018年,腾讯20周年。我30周岁,刚好在腾讯工作满8年。
我从来没有想过自己会在同一家公司工作8年。因为4年足以读完大学,6年能让小孩读完小学,8年漫长得不可思议。
2010年,我刚大学毕业,加入腾讯。那一天,学生思维的我,不免以学生的尺度定计划:三年的时间,我应该足够从这一所“社会大学”毕业吧。
因此,我追赶时间,以这个截止日为目标,第一年学习高效地完成工作,第二年学习带新人,第三年学习影响力,翻译了一本前端书,和一本设计书。
我一步步从助理UI工程师晋级到高级UI工程师,先是积极响应需求,后来主动找事情做。我低着头,做事情非常“用力”,自信能把交给我的事情都做得很好。
我的博客文章80%都是头三年写的,现在回头看有很多幼稚的想法,但持续想和写才能提高。反过来说,要是现在还觉得好,那才糟糕。
2013年,三年之痒。我开始觉得日常工作毫无挑战,考评时连续“优秀”跟“超出预期”拿到手软,但与此同时,也迎来新的工作挑战。
那时,我的领导问我以后的发展意向,是想继续研究技术深度,还是管理团队。我说如果有机会,尽量管理团队吧。
因为以我的理解,并不存在两种选项。这个问题就像“给你加工资,好不好啊”一样没有意义。学而优则仕,骨干不去领导团队,可能有点不负责任(现在想来很自恋,呵呵)。
虽然给了领导这样的答复,也开始进行正式的管理培训,但我内心还恋恋不舍想保留一点匠人心态。
个人能力要继续提高,我就开辟新的赛道:影响力。
2014年我认真地投入到写作练习中,在“26岁总结”中写下这段文字:
“在很多场景下我们都需要写作,我们要写短小的RTX,长一点的邮件,以及更长一点的分享文章、博客和专栏。关于写作,我觉得最有趣的一个事实是,优秀的写作者跟平庸的写作者所能达到的效果相差百倍以上,比优秀程序员和平庸程序员之间的差别还大。”
“优秀的写作者的RTX就是能让对方明白他的目的,并且像施了魔咒一样去合作。优秀的写作者的邮件能让接受者感兴趣,清晰地知道信息。优秀的写作者写的博客能用一段话击中读者心理,情不自禁点右上角的“分享到朋友圈”……这种效果100个平庸的写作者都达不到。”
“写作者需要的除了文笔,还有逻辑思维、数据分析、麦肯锡金字塔理论、心理学等等几乎所有的知识……”
每个人每天都要阅读微信、朋友圈、新闻、读书、知乎、小视频……关于写作对个人能力的的重要性,我认为怎么强调都不为过。因为广义的写作、演讲和设计,它们有一个共同的关键内核,就是搞清楚你的听众是谁,他们已有哪些信息,缺乏哪些信息,你要以怎么样的顺序来传达你想让对方做的事情。讲得邪乎点,它们都是一种“心理操纵术”。
那怎么才能学好写作(或者演讲,或者设计)呢?
答案无趣但有效:持续写。
反复阅读写出来的文字,毫不吝啬地删除无用的信息,重新再写。
达芬奇说过一句话:“Simplicity is the ultimate form of sophistication(简洁是终极的复杂)”。海明威每天写作之前都会把前一天的稿再改一次。
我也这样做,一开始在自己博客上写水文、在豆瓣写书评,感觉不够。2014年2月,我加入豆瓣专栏计划,需要每周写一篇超过3000字的专栏文章,结束后能获得200元鼓励金。我就像一个缺乏运动的人,强行把自己推入马拉松赛道。
我的前几篇写的很业余,错别字、口水话、病句、缺主语、串主语、一逗到底、唠唠叨叨、层级和顺序不对……那也还是要写。几个月后,20篇文章的专栏完结了,我的文字水平稳定从30分提升到50分,接近及格。
因为专栏内容相对新颖(可能是国内首批系统写“全栈工程师”的思考的专栏),慢慢积累一些读者并每周追看。读者宽容并热情地在评论区给我纠错。
后来,人民邮电出版社的责编在豆瓣上看到了我,就约我写稿。他说我写的东西已经很多,也有一些脉络,可以再整理一下出书。又是一个新的挑战。
先答应再说吧。
写书的过程只能说勉力支撑,因为只有50分的文字水平,却要输出80分的质量。把第一章整理好之后发过去,收到返回的修正稿,变成了另一篇文章。责编很专业,没有吐槽,只是做客观订正。
我羞愧难当,因为痛恨给人添麻烦。我记住修改过的问题种类,文法上字斟句酌,保证同类的不再犯。
因为责编会看出文字上的问题,然后给我修剪枝丫,但保持大树根基稳定就是我自己的责任,对读者的责任。我还买来《麦肯锡教我的写作武器》,更系统地学习写作。
经过好多轮的校对,我终于可以坦然说出,差不多达到基本的标准了。后面的事情我在“我出书了”中也都写了。2015年8月,我的书出版了。我在豆瓣上也慎重给《Web全栈工程师的自我修养》打了4星。
写作成长磕磕碰碰的同时,管理之路也迂回曲折。试着带一段时间团队之后,我在2014年正式成为团队管理者。
当时对于团队管理的职责抱有几个不成熟心态:
管理比写代码更容易掌握,践行起来也更轻松
管理者门槛较低,相较于工程师缺乏核心竞争力,以后跳槽我还是要以工程师身份来定位
我喜欢专注做事情,不适合做管理
在工程师团队中,我要以最强的技术和努力来赢得尊重,我要有能力解决他们都解决不了的问题
因此,从2014-2016虽然也通过努力收获了一些个人成长,但对团队领导来讲其实我是不称职的。
有一个明显不称职的表现就是,每到员工考核期间,我就很纠结痛苦。我不希望有员工拿低于预期的考评,也害怕面对下属沟通面谈,当面对着他说你的绩效低于预期。
我能自律勤奋,但我很难改变自己的观念。最难的是,我甚至不知道自己是否应该改变自己的观念(瞧,这就是为什么改变观念是最难的),还是说退回去做一个还不错的工程师好了。
我在这个观念段位大概停留了两年多,经过断断续续的实践、阅读、观察和自省,我终于升级了。期间纠结和思考过程可以从2014到2016的博客文章中可见一斑。
总之,升级后的我认为:
管理比写代码(或任何一门硬技能)更难于掌握,这是我跟一些技术专家沟通得到的共识。硬技能的学习可以通过读书、培训班,甚至网络视频来学习,然后持续练习,越来越熟练,直到产生一个输出物。这非常简单,只要掌握了学习的方法,几个月就能学习一门硬技能。而管理的学习需要真实观念的转变,这个观念改变可能需要几年时间。
管理的核心观念是“管理者必须善于做有效的决策”。
管理者要注重组织对外的贡献。基于贡献来衡量每个人的绩效。
我开始积极与团队沟通,日常中看到不符合要求的输出,我就会直截了当地说“这不行,达不到基本水准”。
虽然比较严格,但也没有看到团队氛围下降的情况。因为从员工角度来讲,虽然乐于处在一个人际关系融洽的团队中,但更大的述求是加入一个充满专业人士的团队。每人都能从其他人身上学到特定知识,每人表现都是专业的。
我不再担心员工考核,因为它是一个有效的管理工具。有些无法用言语传达到的信息,可以通过绩效考核来传达。而且平时对于低绩效的员工就要做好预期管理,言行一致员工就不会困惑。
2017年,我慢慢成为一个资深的管理者。又一次对工作驾轻就熟时,再次迎来新的挑战——转换岗位,领导腾讯微云UX设计团队。
我喜欢这个挑战,一方面它确实是一个“很大挑战”,受虐症的我无法拒绝。另一方面我一直处于产品流程中偏后的位置,但也对前置的思考很感兴趣。
因此,我梳理了自己面临的挑战:
之前领导的团队都是工程师,而现在的团队由交互设计师和视觉设计师组成。虽然管理的基本法是相通的,但新团队的成员还需要更多熟悉
自己的设计专业能力不够,尤其是在视觉上,无法给到“怎么做”的建议
新的UX设计团队面临比以前更复杂的外部关系
如何帮助下属专业晋升
但我也有我的优势:
能轻松地理解版本管理、多平台特性、开发挑战等“工程”难题,然后管理好风险
参加了三年多设计部的管理会,对设计的“味道”有感觉。或者说品味远远高出实现能力
在UI方案上,我有用户同理心,不只是从“好看”来评判设计
我的演讲呈现能力和写作能力可以提升团队
唔,也不全是坏消息嘛。那就开始做吧!
前半年仍然是勉力支撑(哭),但因为团队都在看着自己,不自信也要自信起来。
工作之余多体验各种APP,收集UI、运营、营收、品牌等方面的案例,进一步提升“产品力”。老婆平时在使用新的APP,或者被活动吸引付费时,我就会在边上观察她的行为。看到精彩处,我还会请求暂停,截图发给我。
总之,我希望自己的专业成长能快速补上权限扩大带来的差距。
慢慢地,有一些“腾讯微云用户体验不错”的口碑了,在自己能影响的范围内,使用我能调用的资源,慢慢地补齐漏洞,提升体验。
但仍然能力有限,有时候会出现仓促出的方案不合理的情况。这时更加如履薄冰,不是怕外部批评我,而是怕连累授权给我的上司,和被我能力所限的团队。加上腾讯微云也是一个有历史包袱的产品,所以仍然离自己心中的理想产品差很多。
团队成长上的挑战同样很大。
我仔细观察每一个人的优缺点,用人所长。于此同时我还要横向去看部门其他设计师的输出,不希望相对独立的产品导致了封闭的专业氛围。这将是接下来花半年到一年重点要解决的。
我对现在这个挑战,还远远没到驾轻就熟的状态,可能还需要两年以上时间来消化,所以有时工作会觉得比前几年加起来还累。
我时常以山本耀司的话给自己打鸡血:
我从不相信什么懒洋洋的自由,我向往的自由是通过勤奋和努力实现更广阔的人生,那样的自由才是珍贵的、有价值的。我相信一万小时定律,我从来不相信天上掉馅饼的灵感和坐等的成就。做一个自由又自律的人,靠势必实现的决心认真地活着。
勤奋和努力只是基础,以大多数人的努力程度之低,还根本谈不上拼天赋。
疲劳和兴奋交替,成就和挫折并存,曲折前行,比轻松混日子更有趣。
回头看,这八年来,我从来都只是“短暂地胜任”自己的工作。每当我觉得能够驾轻就熟地处理目前的日常,就会迎来新的挑战。
想到这一点,我突然悟到一种“佛性”的工作哲学:
每当你能胜任当前的工作,就会迎来更高难度的挑战。
每一个能胜任当前工作岗位的人,都会被提拔。继续胜任,那就继续提拔,直到不能胜任。
因此,不用特别在意自己的头衔、权限和职级,外部的认可是你能力的反馈。你没有被提拔,大概率是因为还不胜任当前工作。如果完全胜任还没有被安排更有挑战的工作,要么自己找事情做,要么跳槽转岗。
反过来说,自知自己能力还达不到岗位要求也不用担心,不胜任是常态,以胜任为目标就好啦。
对于职场马拉松来说,心态放松,保持作息,持续养成好的习惯,学习好的方法和观念,这才是最重要的。
我写字的地方迁移到公众号啦~欢迎关注我的公众号:余果专栏
深度学习模型落地需要考虑决定推理(inference)过程所需的计算资源(成本)和效率(系统的吞吐量和延时),有时甚至需要进行适当的模型裁剪和压缩工作。理论上说,模型结构一旦确定是可以计算它的复杂度和计算量,但这有些繁琐。实际中可以借助一些工具帮助预估模型实际的性能,比较模型优化前后的差别,主要使用到的是benchmark_model和summarize_graph。
在深度学习模型工程落地时,我们追求在成本可控的前提下提高良好的用户体验,因此模型的推理效率和计算代价是重要的衡量指标。通常用FLOPs(floating point operations)描述模型的计算力消耗,它表示浮点运算计算量,用来衡量算法/模型的复杂度。我们是可以从原理上计算出模型需要的FLOPs,参考:https://www.zhihu.com/question/65305385。 除了从理论计算之外,还可以使用tensorflow中的 benchmark_model 工具来进行粗略估计,它可以帮助估算出模型所需的浮点操作数(FLOPS),然后你就可以使用这些信息来确定你的模型在你的目标设备上运行的可行性。除此之外,比较容易混淆的概念是FLOPS(floating point operations per second),意指每秒浮点运算次数,理解为计算速度,它是衡量硬件性能的指标对于来说TESLA P40可以每秒处理12T个FLOP,普通单核CPU每秒大概处理100亿次的FLOP。当有了计算操作消耗的估计之后,它就对你计划的目标设备上有所帮助,如果模型的计算操作太多,那么就需要优化模型减小FLOP数量。
例如下面的例子中,我们通过benchmark_model分析resetNet20-cifar10,大概有82.15M的FLOPs,该机器每秒执行21.89B,因此该模型大概需要4ms的计算时间。在使用benchmark_model之前,需要使用tensorflow源码进行编译。
1 | 编译benchmark_model |
1 | 2019-10-11 21:30:31.288678: I tensorflow/tools/benchmark/benchmark_model.cc:636] FLOPs estimate: 82.15M |
1 | ========================= Summary by node type ========================================== |
服务端深度模型落地时主要关注模型的预测效率,移动端模型落地需要考虑模型的大小。通过summarize_graph工具可以帮助我们简要分析模型的参数量和包含哪些op。设置–print_structure=true可以观察到模型的结构,这也可以通过tensorboard来可视化实现。
1 | tensorflow-1.14.0编译summarize_graph工具 |
1 | Found 1 possible inputs: (name=net_input, type=float(1), shape=[?,32,32,3]) |
tensorflow针对训练、预测、服务端和移动端等环境支持多种模型格式,这对于初学者来说可能比较疑惑。目前,tf中主要包括.ckpt格式、.pb格式SavedModel和tflite四种格式的模型文件。SavedModel用于tensorflow serving环境中,tflite格式模型文件用在移动端,后续遇到相关格式模型文件会继续补充。这里主要介绍常见的ckpt和pb格式的模型文件,以及它们之间的转换方法。
在使用tensorflow训练模型时,我们常常使用tf.train.Saver类保存和还原,使用该类保存和模型格式称为checkpoint格式。Saver类的save函数将图结构和变量值存在指定路径的三个文件中,restore方法从指定路径下恢复模型。当数据量和迭代次数很多时,训练常常需要数天才能完成,为了防止中间出现异常情况,checkpoint方式能帮助保存训练中间结果,避免重头开始训练的尴尬局面。有些地方说ckpt文件不包括图结构不能重建图是不对的,使用saver类可以保存模型中的全部信息。尽管ckpt模型格式对于训练时非常方便,但是对于预测却不是很好,主要有下面这几个缺点:
Google推荐将模型保存为pb格式。PB文件本身就具有语言独立性,而且能被其它语言和深度学习框架读取和继续训练,所以PB文件是最佳的格式选择。另外相比ckpt格式的文件,pb格式可以去掉与预测无关的节点,单个模型文件也方便部署,因此实践中我们常常使用pb格式的模型文件。那么如何将ckpt格式的模型文件转化为pb的格式文件呢?主要包含下面几个步骤,结合这几个步骤写了个通用的脚本,使用该脚本只需指定ckpt模型路径、pb模型路径和模型的输出节点,多个输出节点时使用逗号隔开。
1 | # -*-coding: utf-8 -*- |
最近在做模型压缩(model compress)相关工作,之前分别尝试了权重量化(weight quantization)【1】和权重稀疏(weight sparsification)【2】,遗憾的是它们都需要推理引擎和硬件的特定优化才能实现推理加速,而tensorflow在x86架构的CPU下并没有没有针对量化和稀疏矩阵的优化,因此效果一般。吸取前两次的经验,这次尝试了结构化压缩通道剪枝(channel pruning),它通过删减模型中冗余通道channel,减少的模型前向计算所需的FLOPs。通道剪枝来自论文ICCV2017论文 Channel Pruning for Accelerating Very Deep Neural Networks。 这里会首先简单介绍channel pruning的原理,然后通过PocketFlow压缩工具对ResNet56进行通道剪枝,结果显示channel pruning在精度不怎么损失的基础上,减小接近50%的FLOPs。由于剪枝后模型中增加了许多的conv2d 1x1卷积,实际提升推理效率大概20%。
虽然论文末尾谈到channel pruning可以应用到模型训练中,但是文章的核心内容还是对训练好的模型进行channel pruning,也就是文章中说的inference time。通道剪枝正如其名字channel pruning核心思想是移除一些冗余的channel简化模型。下图是从论文中截取的通道剪枝的示意图,它表示的网络模型中某一层的channel pruning。B表示输入feature map,C表示输出的feature map;c表示输入B的通道数量,n表示输出C的通道数量;W表示卷积核,卷积核的数量是n,每个卷积核的维度是ckhkw,kh和kw表示卷积核的size。通道剪枝的目的就是要把B中的某些通道剪掉,但是剪掉后的B和W的卷积结果能尽可能和C接近。当删减B中的某些通道时,同时也裁剪了W中与这些通道的对应的卷积核,因此通过通过剪枝能减小卷积的运算量。
通道剪枝的思想是简单的,难点是怎么选择要裁剪的通道,同时要保证输出feature map误差尽可能得小,这也是文章的主要内容。channel pruning总体分为两个步骤,首先是channel selection,它是采用LASSO regression来做的,通过添加L1范数来约束权重,因为L1范数可以使得权重中大部分值为0,所以能使权重更加稀疏,这样就可以把那些稀疏的channel剪掉;第二步是reconstruction,这一步是基于linear least优化,使输出特征图变化尽可能的小。
接下来通过数学表达式描述了通道剪枝。X($N*c* k_h*k_w$)表示输入feature map,W($n * c * k_h * k_w$)表示卷积核,Y($N*n$)表示输出feature map。$\beta_i$表示通道系数,如果等于0,表示该通道可以被删除。我们期望将输入feature map的channel从c压缩为c’($0<=c’<= c$),同时要使得构造误差(reconstruction error)尽可能的小。通过下面的优化表达式,就可以选择哪些通道被删除。文章中详细介绍了怎么用算法解决下面的数据问题,这里就不赘述了。另外文章还考虑分支情况下的通道剪枝,例如ResNet和GoogleNet,感兴趣的可以仔细研读该论文【3】。
PocketFlow是腾讯AI Lab开源的自动化深度学习模型压缩框架,它集成了腾讯自己研发的和来自其他同行的主流的模型压缩与训练算法,还引入了自研的超参数优化组件,实现了自动托管式模型压缩与加速。PocketFlow能够自动选择模型压缩的超参,极大的方便了算法人员的调参。这里主要使用里面的channel pruning算法(learner)进行通道剪枝。【4】
1.cifar10数据集: https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
2.ResNet56预训练模型:https://share.weiyun.com/5610f11d61dfb733db1f2c77a9f34531
3.下载Pocketflow: https://github.com/wxquare/PocketFlow.git
1 | # data files |
1 | $ ./scripts/run_local.sh nets/resnet_at_cifar10_run.py \ |
步骤3之后会在models产生ckpt文件,需要通过进行模型转化,最终会生成model_original.pb,model_transformed.pb,同时也会生成移动端对应的tflite文件。
1 | $ python tools/conversion/export_chn_pruned_tflite_model.py \ |
我们可以通过之前介绍的模型基准测试工具benchmark_model分别测试剪枝前后的模型。可以很清楚看到通道剪枝大大减少了模型前向计算的FLOPs的变化,以及各阶段、算子的耗时和内存消耗情况。可以发现模型下降为原来的1/2,卷积耗时下降接近50%。除此之外通过netron工具可以直观的看到模型通道剪枝前后结构发生的变化,通道剪枝之后的模型中明显增加了许多conv1*1的卷积。这里主要利用1x1卷积先降维,然后升维度,达到减少计算量的目的。1x1卷积还有多种用途,可以参考【5】。
1 | $ bazel-bin/tensorflow/tools/benchmark/benchmark_model \ |
参考:
[1]. tensorflow模型权重量化(weight quantization)实战
[2]. tensorflow模型权重稀疏(weight sparsification)实战
[3].Channel Pruning for Accelerating Very Deep Neural Networks
[4].PocketFLow
[5].1x1卷积:https://www.zhihu.com/question/56024942
深度模型通常会有更好的预测精度,但是它面临计算开销过大的问题。模型压缩(model compress)是提高深度模型推理效率的一种解决方案,它期望在不损失精度或者精度损失可控的范围内,加速推理效率,减低内存开销。目前,模型压缩算法主要包括权重量化(quantization)、剪枝(pruning)、低秩分解等。上周尝试了tensorflow中的模型量化,发现量化需要硬件或者推理引擎的对低精度8-bit计算支持,目前tensorflow在x86和gpu环境下还没有很好的支持,因此量化只帮助实现了模型大小下降,没有实现推理的加速。model pruning学习的材料是tensorflow repo中的tensorflow/contrib/model_pruning,实际了解后发现它属于pruning中no-structural pruning,其加速效果依赖具体的硬件实现,加速效果一般,tensorflow 中对稀疏矩阵运算没有特别好的优化(依赖于底层的 SparseBLAS 实现,目前还没有特别好的)。model pruning中还有一种structural pruning 则不改变计算方式,可以直接使用,加速效果相对较好,之后也会继续尝试。
Michael Zhu and Suyog Gupta, “To prune, or not to prune: exploring the efficacy of pruning for model compression”, 2017 NIPS
tensorflow中model_pruning理论来自上面这篇文章。文章中指出目前有些深度学习网络模型是过度设计(over-parameterized)。为了使其在资源受限的环境下高效的进行推理预测,要么减少网络的隐藏单元(hidden unit)同时保持模型密集连接结构,要么采用针对大模型进行模型剪枝(model pruning)。文章中的模型行剪枝是一种非结构化的剪枝(no-structural pruning),它在深度神经网络的各种连接矩阵中引入稀疏性(sparsity),从而减少模型中非零值参数的数量。文章比较了大而稀疏(large-sparse)和较小密集(small-dense)这两种模型,认为前者是优于后者的。除此之外,文章提出了一种新的渐进剪枝技术(gradual pruning technique),它能比较方便的融入到模型训练的过程中,使其调整比较小。
tensorflow中的模型剪枝是一种训练时剪枝。对于需要被剪枝的网络模型,对于网络中每个需要被剪枝的层(layer)添加一个二进制掩码变量(binary mask variable ),该变量的大小和形状和改层的权重张量(weight tensor)相同。在训练图中加入一些ops,它负责对该层的权重值(weights)的绝对值进行排序,通过mask将最小的权重值屏蔽为0。在前向传播时该掩模的对应位与选中权重进行相与输出feature map,如果该掩模对应位为0则对应的权重相与后则为0,在反向传播时掩模对应位为0的权重参数则不参与更新。除此之外,文章提出了一种新的自动逐步修剪算法(automated gradual pruning),它实际上是定义了一种稀疏度变化的规则,初始时刻,稀疏度提升较快,而越到后面,稀疏度提升速度会逐渐放缓,这个主要是基于冗余度的考虑。因为初始时有大量冗余的权值,而越到后面保留的权值数量越少,不能再“大刀阔斧”地修剪,而需要更谨慎些,避免“误伤无辜”。其表达式如下,官方文档中列出了一些的剪枝超参数,主要的有下面几个。
$$s_{t}=s_{f}+\left(s_{i}-s_{f}\right)\left(1-\frac{t-t_{0}}{n\Delta t}\right)^{3} $$
tensorflow中model_pruning的源码位于tensorflow/contrib/model_pruning。
准备tensorflow-1.14.0源码
编译model_pruning
1 | $bazel build -c opt tensorflow/contrib/model_pruning/examples/cifar10:cifar10_train |
通过设置一些参数,开始针对cifar10剪枝
1 | $bazel-out/k8-py2-opt/bin/tensorflow/contrib/model_pruning/examples/cifar10/cifar10_train \ |
可通过tensorboard查看剪枝过程。可以清楚的看出随着训练步骤的增加,conv1和conv2的sparsity在不断的增长。 在GRAPHS 页面,双击conv节点,可以看到在原有计算图基础上新增了mask和threshold节点用来做 model pruning
1 | $tensorboard --logdir=/home/terse/code/programming/tensorflow/model_pruning/train |
模型剪枝之后将剪枝的ops从训练图中删除。
1 | $bazel build -c opt tensorflow/contrib/model_pruning:strip_pruning_vars |
使用tensorflow的model_pruning进行模型剪枝,主要包括两方面的工作,一是apply_mask,二是在训练图中增加剪枝的节点(pruning ops)。这里分别截取了其中的两段代码。
1 | # cifar10_pruning.py apply_mask to the graph |
1 | #Adding pruning ops to the training graph |
参考:
最近在尝试深度学习模型加速的工作,查了一些资料,发现模型推理加速的研究还挺多的,主要从四个方面进行,从头开始构建轻量高效的模型,例如mobileNets、squeezenet等;通过量化(quantization)、裁剪(pruning)和压缩(compression)来降低模型的尺寸;通过高效的计算平台加速推理(inference)的效率,例如Nvidia TensorRT、GEMMLOWP、Intel MKL-DNN等以及硬件定制。考虑到自身的能力,遵循从简单到复杂、通用到专用的原则,选择从模型量化(model quantization)入手,之后会陆续尝试其他优化手段。在一番尝试之后,挺遗憾的,因为tensorflow模型量化并没有使模型预测(inference)加速,根据tf成员在issue的回复,tf的模型量化主要针对移动端的优化,目前还没有针对x86和gpu环境的优化。有成功通过模型量化加速推理过程的同学欢迎打脸留言。
为了尽可能保证深度学习模型的准确度(precision),在训练和推理时候通常使用float32格式的数据。然而在实际商用中,有些模型由于层数和参数都比较多,推理预测需要很大计算量,导致推理(inference)的效率很低。模型量化(model quantization)是通用的深度学习优化的手段之一,它通过将float32格式的数据转变为int8格式,一方面降低内存和存储的开销,同时在一定的条件下(8-bit低精度运算 low-precision)也能提升预测的效率。目前还不太理解8-bit低精度运算,猜测这是模型量化没有实现推理加速的原因。模型量化适用于绝大数模型和使用场景,对于训练后的量化,不需要重新训练模型,可以很快将其量化为定点模型,而且几乎不会有精度损失,因此模型量化追求更小的模型和更快的推理速度。实验中量化确实时模型下降为原来的1/4,但在推理效率并没有提升,甚至略有下降。
网络上关于模型量化的内容挺多的,量化本质上是一种仿射图(affine map),它以表达式(1)将实数值表示映射为量化的uint8,当然也可以等效为表示式(2):
1 | real_value = A * quantized_value + B (1) |
除此之外,深度学习模型量化中有一个约束条件,0必须准确的表示,不能有误差。因为对于某些神经网络层,实数0精确表示对于优化实现非常有用,例如在具有填充的卷积层或池化层中,长度对输入数组进行零填充(zero-padding)来实现填充是有用的。实数值0对应的量化值称之为零点(zero-point)。实际上,如果0不能完全表示,当我们用0对应的量化值进行填充时,因为这与实际值0不完全对应,会导致结果不准确,引入偏差。因此有:
1 | 0=A∗zero_point+B |
结合上述条件,可以得出量化的最终表达式为(3),它能做到0值的准确表示,zero_point是0对应的量化值。表示式(3)中有两个常量,zero_point是量化值,通常是uint8值,scale是一个正实数,通常为float32。
$$real\_value = scale * (quantized\_value - zero\_point) (3)$$
根据表达式(3),我们可以将实数值(通常为float32)用量化值(通常为uint8)表示,下面将介绍怎么把它应用到矩阵乘法当中。假设有两个实数矩阵$lhs\_real\_matrix, rhs\_real\_matrix$,量化之后就会有对应的$lhs\_scale, rhs\_scale, lhs\_zero\_point, rhs\_zero\_point$,矩阵中的实数值可以用其量化值表示为:
1 | lhs_real_value[i] = lhs_scale * (lhs_quantized_value[i] - lhs_zero_point) |
在矩阵乘法中,每个值($result\_real\_value$)都由对应的i个值相乘累加得到,根据表达式(4)和(5)很容易得到表示式(6),它表示$result\_quantized\_value$可由$lhs\_quantized\_value、rhs\_quantized\_value$计算得出。注意这里面有几个问题需要解决,如何减小式(6)中与zero_point减法的开销(overhead)?如何将(lhs_scale * rhs_scale / result_scale)实数运算用整数运算处理?这部分的内容参考gemmlowp的实现。
https://github.com/google/gemmlowp/blob/master/doc/quantization.md
1 | result_real_value |
**训练后量化(post training Quantization)**。在许多情况下,我们希望在不重新训练模型的前提下,只是通过压缩权重或量化权重和激活输出来缩减模型大小,从而加快预测速度。“训练后量化”就是这种使用简单,而且在有限的数据条件下就可以完成量化的技术。训练后量化操作简单,只需要使用量化工具将训练好的模型执行量化类型,即可实现模型的量化。训练后量化包括“只对权重量化”和“对权重和激活输出都量化”,对于很多网络而言,都可以产生和浮点型很接近的精度。
**只对权重量化(weight only quantization)**。一个简单的方法是只将权重的精度从浮点型减低为8bit整型。由于只有权重进行量化,所以无需验证数据集就可以实现。一个简单的命令行工具就可以将权重从浮点型转换为8bit整型。如果只是想为了方便传输和存储而减小模型大小,而不考虑在预测时浮点型计算的性能开销的话,这种量化方法是很有用的。
量化权重和激活输出(Quantizing weights and activations)。我们可以通过计算所有将要被量化的数据的量化参数,来将一个浮点型模型量化为一个8bit精度的整型模型。由于激活输出需要量化,这时我们就得需要标定数据了,并且需要计算激活输出的动态范围,一般使用100个小批量数据就足够估算出激活输出的动态范围了。
**训练时量化(Quantization Aware Training)**。训练时量化方法相比于训练后量化,能够得到更高的精度。训练时量化方案可以利用Tensorflow的量化库,在训练和预测时在模型图中自动插入模拟量化操作来实现。由于训练时量化相对麻烦,加上权重量化没有实现加速的期望,所以没有尝试训练时量化,根据文档显示,其大概包括以下几个步骤:
一开始尝试模型量化是因为有个复杂的视频分割模型推理效率很低,期望通过模型量化实现加速,在复杂模型上尝试失败之后,我用label_image的例子再次验证,结果显示也没有加速的效果。这里主要试验了训练后量化,尝试了只对权重量化和权重和激活量化,发现后者比前者性能更差,这里描述权重量化的过程。整个过程是比较简单的,tensorflow有两种量化方式,推荐使用第二种,编译命令行工具进行量化。
在tensorflow r1.0的版本中有个量化的脚本可以提供量化的功能:
1 | $wget "https://storage.googleapis.com/download.tensorflow.org/models/inception_v3_2016_08_28_frozen.pb.tar.gz" |
在较新版本的tf中,quantize_graph.py量化的脚本已经废弃了需要编译tensorflow的源码生成
1 | tensorflow-1.14.0编译transform_graph工具 |
使用summarize_graph分析量化前后的模型区别,权重量化、模型减小、增加了一些和量化和反量化的节点。
1 | tensorflow-1.14.0编译transform_graph工具 |
使用权重量化的模型做推理验证
1 | $ bazel build tensorflow/examples/label_image:label_image |
关于tensorflow模型量化没有实现模型加速的,我查了一些资料,发现出现类似的问题不在少数。根据tensorflow团队成员的回复,截了几个member的答复,大意是目前量化目前针对移动端的优化,当然也有一些移动端的人说速度下降了。tensorflow未来有可能针对intel x86,gpu量化优化,但不知道什么时候支持。
The quantization is aimed at mobile performance, so most of the optimizations are for ARM not x86. We’re hoping to get good quantization on Intel eventually, but we don’t have anyone actively working on it yet.
Quantized ops currently only work on the CPU, because most GPUs don’t support eight-bit matrix multiplications natively. I have just seen that the latest TitanX Pascal cards offer eight-bit support though, so I’m hoping we will be able to use that in the future.
参考:
TVM代码生成的接口和主要类型,可以总结为两个build,两个module,两个function。它提供了两个代码生成的接口,tvm.build和tvm.relay.build,前者是针对算子的代码生成,后者是针对relay计算图的代码生成。在0.7版本中,tvm进行了IR的统一,使得两个build的输入参数类型都可以是IRModule,输出类型都是运行时Module。尽管两个build接口统一了输入类型,但是内部包含的函数类型是不一样的,算子编译时是tvm.tir.function.PrimFunc,而relay图编译时函数类型是tvm.relay.function.Function。TVM在设计时提供了方便的调试功能,通过IRModule的astext函数可以查看ir中间描述,通过运行时module的get_source查看生成的代码。下面通过两个简单的例子查看算子和relay图的ir中间描述和以及对应生成的源代码。
import tvm
from tvm import te
M = 1024
K = 1024
N = 1024
# Algorithm
k = te.reduce_axis((0, K), 'k')
A = te.placeholder((M, K), name='A')
B = te.placeholder((K, N), name='B')
C = te.compute(
(M, N),
lambda x, y: te.sum(A[x, k] * B[k, y], axis=k),
name='C')
# Default schedule
s = te.create_schedule(C.op)
ir_m = tvm.lower(s, [A, B, C], simple_mode=True,name='mmult')
rt_m = tvm.build(ir_m, [A, B, C], target='c', name='mmult')
# print tir
print("tir:\n", ir_m.astext(show_meta_data=False))
# print source code
print("source code:\n",rt_m.get_source())
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
from tvm import relay
from tvm.relay import testing
from tvm.contrib import util
import tvm
# Resnet18 workload
resnet18_mod, resnet18_params = relay.testing.resnet.get_workload(num_layers=18)
with relay.build_config(opt_level=0):
_, resnet18_lib, _ = relay.build_module.build(resnet18_mod, "llvm", params=resnet18_params)
# print relay ir
print(resnet18_mod.astext(show_meta_data=False))
# print source code
print(resnet18_lib.get_source())
通过上面两个例子我们知道tvm代码生成接口上是IRModule到运行时module的转换,它完成tir或者relay ir到目标target代码的编译,例如c或者llvm IR等。下面的流程图描述整个代码的编译流程,深色表示C++代码,浅色表示python代码。算子编译时会首先进行tir的优化,分离出host和device部分,之后会调用注册的target.build.target函数进行编译。relay图编译相比算子稍微复杂一点,核心代码采用C++开发。它会通过relayBuildModule.Optimize进行relay图优化,之后针对module中的每个lower_funcs进行编译之前,合成最终的运行时module,其后部分的编译流程和算子编译相似。
TVM针对不同的target实现了许多的codgen,它完成了tir到目标代码的翻译工作,例如c,llvm ir等。我们也可以根据需求实现自己的codegen,官网提供了一个教程。
[1]. Unified IR RFC,https://github.com/apache/incubator-tvm/issues/4617
[2]. Codegen的实现:https://tvm.apache.org/docs/dev/relay_bring_your_own_codegen.html
最近在做深度学习模型加速的工作,先后尝试了模型权重量化(quantization)、模型权重稀疏(sparsification)和模型通道剪枝(channel pruning)等压缩方法,但效果都不明显。权重量化和稀疏属于非结构化的压缩,需要推理引擎和硬件的优化才能实现推理加速,通道剪枝能直接减少FLOPs,确实能卷积网络的效率,在ResNet56网络中能大概提升卷积50%的速度。在工程实践中,除了通过模型压缩提升推理性能,还可以通过优化推理引擎提高推理效率。目前存在多种开源的推理引擎,我首先尝试了TVM。
为提升深度学习模型的推理效率,设备平台制造商针对自己的平台推出优化的推理引擎,例如NAVIDA的tensorRT,Intel的OpenVINO,Tencent针对移动端应用推出NCNN等。目前,深度学习模型应用广泛,在服务端和移动端都有应用,甚至于特殊的嵌入式场景想,它们都有加速模型推理的需求。个人感觉针对不同平台选择不同的推理引擎,学习成本太高。我这里选择尝试TVM,主要有以下几个原因:
看了几篇TVM介绍文章后,了解到它是从深度学习编译器的角度来做推理引擎,目前技术领域还比较新,具体技术细节以后有机会会深入学习,这里主要想体验一下使用TVM做深度模型推理,重点是推理效率的提升,因为是骡子还是马得拉出来遛遛。参考官方文档进行编译安装,整个过程还是比较简单的,结果显示相比于tensorflow大概100%的性能提升。实验环境是ubuntu 19.04,x86_64架构。
1 | $ sudo apt-get install llvm |
1 | $ git clone --recursive https://github.com/dmlc/tvm.git |
1 | export TVM_HOME=/home/xxxx/code/tvm |
1 | # install antlr |
1 | ============ TVM ============ 0.2770531177520752 |
过程遇到一个坑,查了TVM社区,没有很好的解答,看起来好像会和性能有关,希望路过的大佬能帮忙解决。https://discuss.tvm.ai/t/cannot-find-config-for-target-llvm-when-using-autotvm-in-tensorflow-example-for-cpu/1544
1 | WARNING:autotvm:Cannot find config for target=llvm, workload=('conv2d', (1, 8, 8, 2048, 'float32'), (1, 1, 2048, 384, 'float32'), (1, 1), (0, 0), (1, 1), 'NHWC', 'float32'). A fallback configuration is used, which may bring great performance regression. |
参考:
坚持了接近一年的视频算法相关的项目,老板最终还是喊停了。并没有感到特别意外,只是在对一个东西突然有些兴趣或者说入门的时候,戛然而止,多少有些不甘心和遗憾,但是以后会在业余继续学习的,也希望自己在2020年能把工作逐渐聚焦到这块吧。
接触TVM到有两个原因。一是需要支持多种优化手段的推理引擎,例如量化、图优化、稀疏优化、模型压缩剪枝等。尝试过在tensorflow的quantization和非结构性剪枝(no-structural pruning),加速效果非常一般,因为这些优化手段需要推理引擎的支持,但是当时我们都是纯后台出身,也没人掌握这个内容。再之后尝试channel pruning,终于取得了一些进展,但是30%的提升leader并不满意。二是需要支持多种平台的推理引擎,例如NV GPU/x86/ARM GPU等。由于组内业务迟迟没有好的落地场景,尝试了多种手段,需要的把深度模型部署在不同的平台上。记得有次,花了两周的时间把DaSiamRPN模型移植到终端上。从零开始pytorch、onnx、tflite、android,期间踩了许多的坑,结果在移动端运行需要4秒时间来处理一帧图像。。。期间同事也曾通过tensorRT部署模型,效率反而下降。一次偶然的机会了解到TVM,当时感觉它可能是比较适合我们团队的需求的。
由于我之前学习信号处理的,比较容易理解量化。模型量化quantization也在深度学习在部署落地时提高效率的常用的方法。之前有写过关于tensorflow模型量化的方法,写得不好,对于想学习模型量化知识的可以参考下面链接进行学习:
模型量化相关:
【1】神经网络量化简介
【2】Tensort量化:8-bit-inference-with-tensort
【3】阮一峰:浮点数的二进制表示
【4】Quantizing deep convolutional networks for efficient inference
TVM量化相关RFC
【INT8 quantization proposal】:https://discuss.tvm.ai/t/int8-quantization-proposal/516(2018.02.02)
【TVM quantizationRFC】 https://github.com/apache/incubator-tvm/issues/2259(2018.12.09)
目前,官网上还没有关于模型量化的教程和文档,对于刚接触新手来说可能有些麻烦,这里提供提供一个参考代码,方便新手学习。除此之外,也测试了TVM的int8量化性能,结果显示TVM的量化加速效果不是很好,甚至略有下降,需要配合autotvm一起使用。测试代码地址。测试结果如下,希望对大家了解TVM有帮助。
模型 | 原始框架 | 原始框架运行时间 | TVM FP32 | TVM int8 | TVM int8+AutoTVM |
---|---|---|---|---|---|
resnet18v1 | mxnet 1.5.1 | 27.8ms | 46.9ms | 51.10ms | 25.83ms |
Inceptionv1 | tensorflow 1.13 | 560ms | 164ms | 185ms | 116ms |
这周没什么产出,在TVM社区闲逛。。。
参考:
【1】 [Dive into Deep Learning Compiler](Dive into Deep Learning Compiler “http://tvm.d2l.ai/“)
【2】 https://tvm.ai/
在2018年的CVPR上SiameseRPN模型被提出,它宣称在单目标跟踪问题上做到了state-of-the-art,能同时兼顾精度(accuracy)和速度(efficiency)。在这之后,很快又在ECCV上发表了DaSiamRPN模型,它在SiameseRPN基础进一步提升了追踪的性能。SiameseRPN不是一簇而就的,它的设计思想来源于SiameseFc,并引入物体检测领域的区域推荐网络(RPN),通过网络回归避免了多尺度测试,同时得到更加精准的目标框和目标的位置。实际使用中发现DaSiamRPN相比传统的KCF效果直观感受确实精度有较大提升,在普通pc无GPU环境上大概是10.6fps。这里主要结合SimeseRPN的论文和DaSiamRPN的代码帮助了解SimeseRPN的模型结构以及DaSiamRPN的运行过程。
Siamese-RPN本质上是组合网络模型,它包括用于特征提取的Siamese网络和生成候选区域的RPN网络。
Siamese特征提取网络:它目前在追踪领域使用比较多,包括模板分支(template branch)和检测分支(detection branch),它们都是经过裁剪的AlexNet卷积网络,用于提取图像的特征。两个分支网络参数和权重值完全相同,只是输入不同,模板分支输入模板帧中的目标部分(target patch),检测分支输入当前需要追踪的帧的区域(target patch)。
RPN(region proposal subnetwork)候选区域生成网络:它包括的分类(classification)和回归(regression)两个分支。这里有个重要的锚点(anchor),就是通过RPN对每个锚点上的k个不同宽度和高度的矩形分类和回归,得到感兴趣区域。每个anhcor box要分前景和背景,所以cls=2k;而每个anchor box都有[x, y, w, h]对应4个偏移量,所以reg=4k。
因此设模板分支输入为$z$维度为(127,127,3),首先通过Siamese网络特征提取得到$ψ(z)$维度为(6,6,256),然后再经历卷积分别的到$[ψ(z)]{cls}$和$[ψ(z)]{res}$。检测分支输入为$x$,$ψ(x)$为Siamese特征提取网路的输出,以$[ψ(z)]{cls}$和$[ψ(z)]{res}$为核卷积得到最终的SiameseRPN的输出,$*$表示卷积运算。
$$A_{w×h×2k}^{cls} = [ψ(x)]{cls} * [ψ(z)]{cls}$$
$$A_{w×h×4k}^{res} = [ψ(x)]{res} * [ψ(z)]{res}$$
DaSiamRPN做视频目标追踪,DaSiamRPN相比SiameseRPN做了进一步的优化,例如训练时引入采样策略控制不平衡的样本分布,设计了一种distractor-aware模块执行增量学习等。结合官方的https://github.com/foolwood/DaSiamRPN 中的例子,很容易将demo运行起来。需要注意的是github上的代码需要gpu运行环境,如果要在无gpu机器上运行DaSiamRPN的demo需要将有关cuda代码去掉。例如将将net.eval().cuda()换成net.eval()。DaSiamRPN的运行包含两个步骤:
1 | # generate the refined top K proposals |
1 | # size penalty |
1 | target = delta[:, best_pscore_id] / scale_z |
参考:
TVM主要包括两个部分,一个是Relay和图优化(graph-level),另一个就是算子(operator)级别优化,这里简单写最近了解到的关于relay和图优化方面的东西。我们都知道深度学习网络通常都是通过计算图来描述的,计算图中的节点表示各种同的算子(opertor),边表示算子之间的依赖关系。Relay可以理解为一种可以描述深度学习网络的函数式编程语言,通过relay可以描述复杂的深度网络,文中提到了control flow。最近一段时间的时间学习直观的感受的Relay编写网络模型和其它框架没什么太多的区别,但是提供的文本形式的中间表示,对开发和调试有很大的帮助。另外,它提供了许多用于图优化的pass,供大家学习和参考。测试代码都在0.6版本上调试通过。
代码地址:https://github.com/wxquare/programming/tree/master/blog/TVM_graph_optimization
既然Relay是一种可以描述计算的函数式语言,逛社区的发现一段代码,可以当作Relay的第一个程序。
API参考:https://docs.tvm.ai/api/python/relay/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from tvm import relay
import tvm.relay.op
x = relay.expr.var('x', relay.scalar_type('int64'), dtype = 'int64')
one = relay.expr.const(1, dtype = 'int64')
add = relay.op.tensor.add(x, one)
func = relay.expr.Function([x], add, relay.scalar_type('int64'))
mod = relay.Module.from_expr(func) # note this API
print("Relay module function:\n", mod.astext(show_meta_data=False))
graph, lib, params = tvm.relay.build(mod, 'llvm', params={})
print("TVM graph:\n", graph)
print("TVM parameters:\n", params)
print("TVM compiled target function:\n", lib.get_source())
在学习Relay的时候参考了https://zhuanlan.zhihu.com/p/91283238 这篇文章。但是可能因为版本的问题,很多API多不兼容了,因此修改了一些地方,建议读者也可以去看一下。
1 | import tvm |
前面两个例子介绍了怎么使用relay构建网络,这个部分介绍怎么使用relay做图优化。上面例子代码中没有直接图优化的代码,而是包含在relay.build中。通过追踪代码,我们这部分的逻辑集中在 https://github.com/apache/incubator-tvm/blob/v0.6/src/relay/backend/build_module.cc 这个文件的optimize函数中。这里罗列了代码用到的pass,relay提供了方便的的文本形式中间描述,感兴趣的可以自己试一下每个pass之后,发生了哪些变化。
TVM核心代码是采用C++编写的,但是也提供了Python接口,这方面初学者体验的使用。Relay图优化核心功能都提供了对应的API,因此可以尝试一下,非常简单。
1 | def my_optimize(func,params=None): |
这里可以对比优化前后的IR.
1 | Relay module function: |
参考与进阶学习:
[1]. https://www.zhihu.com/question/331611341/answer/875630325
[2]. https://zhuanlan.zhihu.com/p/91283238
[3]. https://docs.tvm.ai/dev/relay_intro.html
[4]. https://docs.tvm.ai/dev/relay_add_op.html
[5]. https://docs.tvm.ai/dev/relay_add_pass.html
[6]. https://arxiv.org/pdf/1810.00952.pdf
在《初识TVM,相比于tensorflow的2倍性能提升》之后,最近花了一点业余时间了解TVM及其周边,并进行相应的性能测试。整体感受是计算优化(GEMM)是非常繁杂的工程工作,需要花费大量的时间和精力才能有比较好的效果。numpy非常优秀,大矩阵乘法硬件利用率在90%以上。TVM在GEMM优化上能实现和numpy相当的效果,重要的是它能大大简化工作量。参考了一些文章,这里简单罗列了几个知识点和测试数据。
对于性能优化来说,了解硬件的性能指标是非常有必要的。在Linux系统上可以通过/proc/cpuinfo文件看看机器的配置。比如CPU主频、CPU核数core、cache大小、是否支持向量指令SSE、AVX2、AVX512等,这些对于计算性能有非常大的影响。浮点峰值那些事儿。通常我们使用浮点计算能力来衡量硬件的性能,对于多核服务器来说,频率为2.4G,支持AVX2,FMA向量指令,单核性能如下:
对于float32理论峰值为2.4G * (8+8) * 2 = 76.8 GFLOPS
对于float64理论峰值为2.4G * (4+4) * 2 = 38.4 GFLOPS
numpy非常优秀,我们通过矩阵乘法了解其性能数据。测试机器为一台多核的服务器,主频是2.4G,支持FMA和AVX2向量指令。测试了不同size矩阵相乘的性能数据。分别测试了单核和四核状态下对float32和float64的不同size(32,128,1024,2048等)矩阵相乘的性能数据。测试结果显示numpy在大矩阵相乘中,硬件利用率大概在90%左右。
name | 32 | 128|1024|2048|4096|10240|硬件利用率|
-|-|-|
单核float32|1.82|36.16|67.99|67.94|68.88|69.88|91.0%
单核float64|1.67|19.49|35.56|35.40|36.11|36.90|96.1%
四核float32|6.6|52.2|225.42|246.2|244.2|256.0|83.8%
四核float64|5.56|37.62|116.42|120.39|127.03|141.15|91.9%
测试代码
通用矩阵乘(GEMM)是计算领域非常基础且核心的工作,目前已有大量的工作,这里就不赘述了。大体上通过分块来减少访存次数、存储顺序、提高cache命中率、利用寄存器提高效率、利用SSE等向量指令提高计算效率等方法。https://github.com/flame/how-to-optimize-gemm/wiki 一步一步详细介绍了GEMM优化的过程,这里在此基础上增加FMA指令的使用,测试了其在1024*1204矩阵相乘的硬件利用率:
name | 64 | 256 |512|1024|硬件利用率|主要优化点|
-|-|-|
MMult0|1.51|0.79|0.66|0.65|1.69%|base
MMult_1x4_5|2.15|1.08|0.72|0.716|2.6%|一次计算1x4个数
MMult_1x4_9|4.90|3.15|3.10|3.14|8.18%|1x4,寄存器
MMult_4x4_5|2.76|1.53|1.26|1.26|3.28%|一次计算4x4个数
MMult_4x4_9|5.19|2.92|2.88|2.87|7.47%|4x4,寄存器
MMult_4x4_10|5.95|4.16|4.04|4.01|10.4%|4x4,寄存器,SSE
MMult_4x4_10_1|10.0|6.6|6.35|6.4|16.7%|4x4,寄存器,FMA
MMult_4x4_11_1|14.5|8.95|7.16|7.08|18.4%|4x4,寄存器,FMA,分块(缓存)
MMult_4x4_15_1|11.3|11.6|11.7|11.7|30.4%|4x4,寄存器,FMA,分块,内存顺序
TVM官网上有关于其针对GEMM的优化的schedule,这里也不赘述了,感兴趣的可以参考后面的参考文章进一步学习,这里测试了在1024*1024矩阵乘法的效率以及其和numpy的比较,可以看出TVM在简单编码的基础上能达到和numpy相当的性能。
| TVM运行时间 | numpy运行时间 |
-|-|-|
baseline|2.49s|0.0135s
blocking|1.73s|0.012s
vectorization|0.411s|0.0117s
loop permutaion|0.104s|0.0116s
packing|0.0987s|0.0103s
write_cache|0.0926s|0.01158s
parallel|0.018s|0.012s
auto-tvm|0.014s|0.0112s
每个阶段测试代码
参考学习链接:
1、浮点峰值那些事儿https://zhuanlan.zhihu.com/p/28226956
2、通用矩阵乘(GEMM)优化算法,https://jackwish.net/gemm-optimization.html
3、如何利用TVM快速实现超越Numpy(MKL)的GEMM。https://zhuanlan.zhihu.com/p/75203171
4、tutorial:https://docs.tvm.ai/tutorials/optimize/opt_gemm.html
5、d2ltvm:http://tvm.d2l.ai/chapter_cpu_schedules/index.html
6、https://github.com/flame/how-to-optimize-gemm
首先回答另一个问题,怎样的系统算是稳定的?
Google SRE中(SRE三部曲[1])有一个层级模型来描述系统可靠性基础和高层次需求(Dickerson’s Hierarchy of Service Reliability),如下图:
该模型由Google SRE工程师Mikey Dickerson在2013年提出,将系统稳定性需求按照基础程度进行了不同层次的体系化区分,形成稳定性标准金字塔模型:
从金字塔模型我们可以看到构建维护一个高可用服务所需要做到的几方面工作:
系统链路梳理是所有保障工作的基础,如同对整体应用系统进行一次全面体检,从流量入口开始,按照链路轨迹,逐级分层节点,得到系统全局画像与核心保障点。
一个系统往往存在十几个甚至更多流量入口,包含HTTP、RPC、消息等都多种来源。如果无法覆盖所有所有链路,可以从以下三类入口开始进行梳理:
对于复杂场景可以做节点分层判断
流量入口就如同线团中的线头,挑出线头后就可按照流量轨迹对链路上的节点(HSF\DB\Tair\HBase等一切外部依赖)按照依赖程度、可用性、可靠性进行初级分层区分。
完成该项梳理工作后,我们应该产出以下数据:对应业务域所有核心链路分析,技术&业务强依赖、核心上游、下游系统、资损风险应明确标注。
站在监控的角度看,我们的系统从上到下一般可以分为三层:业务(Biz)、应用(Application)、系统(System)。系统层为最下层基础,表示操作系统相关状态;应用层为JVM层,涵盖主应用进程与中间件运行状态;业务层为最上层,为业务视角下服务对外运行状态。因此进行大促稳定性监控梳理时,可以先脱离现有监控,先从核心、资损链路开始,按照业务、应用(中间件、JVM、DB)、系统三个层次梳理需要哪些监控,再从根据这些索引找到对应的监控告警,如果不存在,则相应补上;如果存在则检查阈值、时间、告警人是否合理。
监控系统一般有四项黄金指标:延时(Latency), 错误(Error),流量(Traffic), 饱和度(Situation),各层的关键性监控同样也可以按照这四项指标来进行归类,具体如下:
是不是每项监控都需要告警?答案当然是否定的。建议优先设置Biz层告警,因为Biz层我们对外服务最直观业务表现,最贴切用户感受。Application&System层指标主要用于监控,部分关键&高风险指标可设置告警,用于问题排查定位以及故障提前发现。对于一项告警,我们一般需要关注级别、阈值、通知人等几个点。
完成该项梳理工作后,我们应该产出以下数据:
不同于高可用系统建设体系,大促稳定性保障体系与面向特定业务活动的针对性保障建设,因此,业务策略与数据是我们进行保障前不可或缺的数据。
一般大促业务数据可分为两类,全局业务形态评估以及应急策略&玩法。
该类数据从可以帮助我们进行精准流量评估、峰值预测、大促人力排班等等,一般包含下面几类:
容量规划的本质是追求计算风险最小化和计算成本最小化之间的平衡,只追求任意其一都不是合理的。为了达到这两者的最佳平衡点,需尽量精准计算系统峰值负载流量,再将流量根据单点资源负载上限换算成相应容量,得到最终容量规划模型。
对于一次大促,系统峰值入口流量一般由常规业务流量与非常规增量(比如容灾预案&业务营销策略变化带来的流量模型配比变化)叠加拟合而成。
节点容量是指一个节点在运行过程中,能够同时处理的最大请求数。它反映了系统的瞬时负载能力。
1)Little Law衍生法则
不同类型资源节点(应用容器、Tair、DB、HBASE等)流量-容量转化比各不相同,但都服从Little Law衍生法则,即:
节点容量=节点吞吐率×平均响应时间
2)N + X 冗余原则
在满足目标流量所需要的最小容量基础上,冗余保留X单位冗余能力
X与目标成本与资源节点故障概率成正相关,不可用概率越高,X越高
对于一般应用容器集群,可考虑X = 0.2N
要想在大促高并发流量场景下快速对线上紧急事故进行响应处理,仅仅依赖值班同学临场发挥是远远不够的。争分夺秒的情况下,无法给处理人员留有充足的策略思考空间,而错误的处理决策,往往会导致更为失控严重的业务&系统影响。因此,要想在大促现场快速而正确的响应问题,值班同学需要做的是选择题(Which),而不是陈述题(What)。而选项的构成,便是我们的业务&系统预案。从执行时机与解决问题属性来划分,预案可分为技术应急预案、技术前置预案、业务应急预案、业务前置预案等四大类。结合之前的链路梳理和业务评估结果,我们可以快速分析出链路中需要的预案,遵循以下原则:
完成该项梳理工作后,我们应该产出以下数据:
阶段性产出-全链路作战地图
进行完上述几项保障工作,我们基本可得到全局链路作战地图,包含链路分支流量模型、强弱依赖节点、资损评估、对应预案&处理策略等信息。大促期间可凭借该地图快速从全局视角查看应急事件相关影响,同时也可根据地图反向评估预案、容量等梳理是否完善合理。
作战手册是整个大促保障的行动依据,贯穿于整个大促生命周期,可从事前、事中、事后三个阶段展开考虑。整体梳理应本着精准化、精细化的原则,理想状态下,即便是对业务、系统不熟悉的轮班同学,凭借手册也能快速响应处理线上问题。
事前
1)前置检查事项清单
事中
实战沙盘演练是应急响应方面的最后一项保障工作,以历史真实故障CASE作为应急场景输入,模拟大促期间紧急状况,旨在考验值班同学们对应急问题处理的响应情况。
一般来说,一个线上问题从发现到解决,中间需要经历定位&排查&诊断&修复等过程,总体遵循以下几点原则:
应对单库或某单元依赖因为自身原因(宿主机或网络),造成局部流量成功率下跌下跌。
Google SRE中,对于紧急事故管理有以下几点要素:
学习资料:
业务上需要考关注失败、丢失、重复三个问题:
三种语义:
实践
Kafka消息发送有两种方式:同步(sync)和异步(async),默认是同步方式,可通过producer.type属性进行配置。Kafka通过配置request.required.acks属性来确认消息的生产:
综上所述,有6种消息生产的情况,下面分情况来分析消息丢失的场景:
通常来说,producer 采用at least once方式
在实际业务场景中,由于consumer消费速度慢于producer的速度,会造成消息堆积,最终会导致消息过期删除丢失。业务需要监控这种lag情况,并及时告警出来。
另外需要注意的是,kafka只允许单个分区的数据被一个消费者线程消费,如果消费者越多意味着partition也要越多。
然而在分区数量有限的情况下,消费者数量也就会被限制。在这种约束下,如果消息堆积了该如何处理?
消费消息的时候直接返回,然后启动异步线程去处理消息,消息如果再处理的过程中失败的话,再重新发送到kafka中。
Rebalance本身是Kafka集群的一个保护设定,用于剔除掉无法消费或者过慢的消费者,然后由于我们的数据量较大,同时后续消费后的数据写入需要走网络IO,很有可能存在依赖的第三方服务存在慢的情况而导致我们超时。Rebalance对我们数据的影响主要有以下几点:
Kafka虽然除了具有上述优点之外,还具有高性能、高吞吐、低延时的特点,其吞吐量动辄几十万、上百万。
1 | c.Producer.MaxMessageBytes = 1000000 |
创建topic
1 | bin/kafka-topics.sh --create --topic topic-name --replication-factor 2 --partitions 3 --bootstrap-server ip:port |
查看topic情况
1 | bin/kafka-topics.sh --topic topic_name --describe --bootstrap-server broker |
查看消费组情况
1 | ./bin/kafka-consumer-groups.sh --describe --group group_name --bootstrap-server brokers |
重置消费offsets
1 |
|