通用商品模型

通用商品缓存架构

通用订单模型

支付订单表(pay_order_tab):主要用于记录用户的支付信息。主键为 pay_order_id,标识唯一的支付订单。

  • user_id:用户ID,标识支付的用户。
  • payment_method:支付方式,如信用卡、支付宝等。
  • payment_status:支付状态,如已支付、未支付等。
  • pay_amount、cash_amount、coin_amount、voucher_amount:支付金额、现金支付金额、代币支付金额、优惠券使用金额。
  • 时间戳字段包括创建时间、初始化时间和更新时间

订单表(order_tab):记录用户的购买订单信息。主键为 order_id。

  • pay_order_id:支付订单ID,作为外键关联支付订单。
  • user_id:用户ID,标识购买订单的用户。
  • total_amount:订单的总金额。
  • order_status:订单状态,如已完成、已取消等。
  • payment_status:支付状态,与支付订单相关。
  • fulfillment_status:履约状态,表示订单的配送或服务状态。
  • refund_status:退款状态,用于标识订单是否有退款

订单项表(order_item_tab:记录订单中具体商品的信息。主键为 order_item_id。

  • order_id:订单ID,作为外键关联订单。
  • item_id:商品ID,表示订单中的商品。
  • item_snapshot_id:商品快照ID,记录当时购买时的商品信息快照。
  • item_status:商品状态,如已发货、退货等。
  • quantity:购买数量。
  • price:商品单价。
  • discount:商品折扣金额

退款表(refund_tab):记录订单或订单项的退款信息。主键为 refund_id。

  • order_id:订单ID,作为外键关联订单。
  • order_item_id:订单项ID,标识具体商品的退款。
  • refund_amount:退款金额。
  • reason:退款原因。
  • quantity:退款的商品数量。
  • refund_status:退款状态。
  • refund_time:退款操作时间。

实体间关系:

支付订单与订单:

  • 一个支付订单可能关联多个购买订单,形成 一对多 关系。
    例如,用户可以通过一次支付购买多个不同的订单。

订单与订单商品:

一个订单可以包含多个订单项,形成 一对多 关系。
订单项代表订单中所购买的每个商品的详细信息。

订单与退款:

  • 一个订单可能包含多个退款,形成 一对多 关系。
  • 退款可以是针对订单整体,也可以针对订单中的某个商品

通用订单状态机

主从架构中如何获取最新的数据,避免因为主从延时导致获得脏数据

策略 优点 缺点
1. 直接读取主库 - 一致性: 始终获取最新的数据。 - 性能: 增加主库的负载,可能导致性能瓶颈。
- 简单性: 实现简单直接,因为它直接查询可信的源。 - 可扩展性: 主库可能成为瓶颈,限制系统在高读流量下有效扩展的能力。
2. 使用VersionCache与从库 - 性能: 分散读取负载到从库,减少主库的压力。 - 复杂性: 实现更加复杂,需要进行缓存管理并处理潜在的不一致性问题。
- 可扩展性: 通过将大部分读取操作卸载到从库,实现更好的扩展性。 - 缓存管理: 需要进行适当的缓存失效处理和同步,以确保数据的一致性。
- 一致性: 通过比较版本并在必要时回退到主库,提供确保最新数据的机制。 - 潜在延迟: 从库的数据可能仍然存在不同步的可能性,导致数据更新前有轻微延迟。

订单支付核心逻辑

订单履约核心逻辑

基本步骤

  • hexo 文档:https://hexo.io/zh-cn/docs/
  • 安装git、node、npm、hexo、next主题
  • 在github建立wxquare.github.io仓库,两个branch,分别为master和hexo
  • 使用markdown在hexo branch 写文章,hexo generate生成静态文件,并通过hexo deploy 部署到远端
  • 申请域名wxquare.top,绑定wxquare.github.io
  • https://wxquare.github.io/

写文章发布blog的流程

  • 在hexo branch /source/_posts 下使用markdown写文章
  • 使用hexo genergate 生成静态文件
  • hexo server 查看本地效果
  • hexo deploy 到远端
  • 提交修改文件到hexo
1
2
3
4
5
npm i -g hexo  安装hexo
hexo init 初始化
hexo generate 生成静态网页
hexo server 启动服务器 (浏览器输入http://localhost:4000/验证是否正确。)
hexo deploy 部署到远端 (wxquare.github.io)

hexo 配置

  1. 修改站点配置文件_config.yml,使得能将本地博客部署到github上
    1
    2
    3
    4
    deploy:
    type: git
    repo: https://github.com/wxquare/wxquare.github.io.git
    branch: master

next主题 配置

  1. 主题设置和修改。hexo初始化默认的主题是landscape,https://hexo.io/themes/提供了许多的主题,根据喜好为博客的主题,主题的文档提供了使用方法,设置相应的参数,调整为自己喜欢的格式。我这里选择的Next主题
  2. 安装next主题:https://theme-next.js.org/docs/getting-started/
  3. 主题设置:https://theme-next.js.org/docs/theme-settings/

其它

  1. 增加分类
  2. hexo 增加支持markdown公式:http://stevenshi.me/2017/06/26/hexo-insert-formula/
  3. 博客中的图片,将图片放在hexo分支的source/images目录下面,markdown和blog中均可以看到
  4. Hexo博客Next主题添加统计文章阅读量功能:https://bjtu-hxs.github.io/2018/06/12/leancloud-config/

  最近在项目中使用到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 $$

  • $I^*$表示标记帧(参考帧),$I_t$表示当前帧
  • ROI表示参考区域(参考模板,reference template)
  • $H$ 表示参考帧$I^*$到当前帧的的单应矩阵Homography
  • $x$ 表示图像中的一个像素点
  • $w(x,H)$ 表示标记帧上像素点$x$根据单应矩阵$H$到当前帧的映射关系

  这里使用经典的高斯牛顿法(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.计算关键帧中的参考区域中(reference template)中每个像素点的雅克比:

  • 计算关于x方向的梯度
  • 计算关于y方向的梯度
  • 对ROI中的每个点uv计算$J=[d_xu,d_xv,d_x,d_yu,d_yv,d_y,-d_xu^2-d_yuv,-d_xuv-d_yv^2]$
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # img0 表示标记帧
    dx = cv2.Sobel(img0, cv2.CV_64F, 1, 0, ksize)
    dy = cv2.Sobel(img0, cv2.CV_64F, 0, 1, ksize)
    img0 = cv2.GaussianBlur(img0, (ksize, ksize), 1)

    # uv表示标记帧参考区域的每个像素点
    juv = [dx[uv] * u, dx[uv] * v, dx[uv], dy[uv] * u, dy[uv] * v, dx[uv],
    -dx[uv] * u * u - dy[uv] * u * v, -dx[uv] * u * v - dy[uv] * v * v]
    J = np.array(juv).T

    # MJ=-(JT*J)^-1 *JT
    MJ = -np.dot(np.linalg.pinv(np.dot(J.T, J)), J.T)

2.迭代计算当前帧的H的矩阵

  • 迭代条件停止的条件,两次迭代误差小于一个指定值,例如$10^{-8}$
  • 第一次为单位矩阵,之后为上一帧的追踪结果
  • 根据H矩阵将关键帧上上参考区域的点映射到当前帧: uv1 = np.dot(H, uv)
  • 计算关键帧上参考区域到当前帧的误差e:E = img0[uv] - img1[uv1]
  • 计算$\triangledown H = -(J^TJ)^{-1}J_ne_n$
  • 计算新的$H$
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # for deltaH
    MJ = -np.dot(np.linalg.pinv(np.dot(J.T, J)*lambdaJTJ), J2.T)
    #MJ = -np.dot(np.linalg.pinv(np.dot(J2.T, J2)*lambdaJTJ), J2.T)
    deltaH =alpha* np.dot(MJ, E2)

    # for newH
    dh = np.insert(deltaH, 8, np.zeros(1), 0).reshape((3, 3))
    dhinv = np.linalg.pinv(np.identity(3) + dh)
    newH = np.dot(H, dhinv)

实际实现考虑点及其存在的问题

为提高模板追踪算法的效率,visp库在实现模板追踪算法的时候设置了一些可调的参数:

  • 对参考模板中的像素点进行采样处理setSampling
  • 迭代时设置学习率,setLambda默认为0.001
  • 设置最大迭代次数,setIterationMax(200)
  • 设置金字塔的层数,tracker.setPyramidal(2, 1)

  实际使用visp模板追踪算法中,发现当参考模板处有物体遮挡时,效果不好,因此需要做进一步的处理。另外,我们在工程实践时,为了提高追踪的效率,升级了一个并行版本的追踪,能提高数倍的追踪效率。

参考链接:

  最近在做kcf算法在移动端优化的相关工作,由于kcf算法计算量太大,而移动端计算性能有限,因此打算将kcf部分耗时操作通过GPU计算进行提升算法的性能。由于接触GPU和OpenCL的时间比较短,原理性的东西理解得也不深刻,本文主要在移动端测试了一些GPU和OpenCL的数据,无法分析内在原因,方便后续移动端算法优化。主要工作如下:

  1. 编译了OpenCL的opencv版本sdk,测试了mat到umat相互内存拷贝和cvtcolor函数的性能。
  2. 测试了OpenCL核心API的性能
  3. 以内存拷贝核函数为例,测试OpenCL work_item数量与效率的关系。
  4. 测试OpenCL多commandqueue的性能

一、opencv+OpenCL

1.1 编译opencv+OpenCL的sdk

  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 .. 

1.2 测试mat到umat的相互转换的性能

  在编译好opencv sdk之后,首先简单测试了一下sdk是否使用到了GPU资源。测试图片从CPU拷贝到GPU的的性能,opencv提供两组API。UMat::copyTo(OutputArray dst)和Mat::getMat(int access_flags),实际测试中发现copyto那组性能比get的性能更好些,mat.getUmat函数会报错,还不知道什么原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void testMatCopyToUmat(const char* img, int times) {
cv::Mat image = cv::imread(img, cv::IMREAD_UNCHANGED);
cv::Mat out;
cv::UMat u_img;
if (u_img.empty()){
//
}
struct timeval start, end;
struct timeval last_time;
gettimeofday(&start, NULL);
last_time = start;
for (int i = 0; i < times; i++) {
image.copyTo(u_img);
//cv::cvtColor(image, out, cv::COLOR_BGR2GRAY);
gettimeofday(&end, NULL);
P("mat.copyToUmat:%d,run times:%d, spend:%d us", i,times, (end.tv_sec - last_time.tv_sec) * 1000000 +
(end.tv_usec - last_time.tv_usec));
last_time = end;
}
gettimeofday(&end, NULL);
P("mat.copyToUmat: run times:%d, spend:%d ms", times, (end.tv_sec - start.tv_sec) * 1000 +
(end.tv_usec - start.tv_usec)/1000);
}
void testUMatCopyToMat(const char* img, int times) {
cv::Mat image = cv::imread(img, cv::IMREAD_UNCHANGED);
cv::Mat out;
struct timeval start, end,last_time;
cv::UMat u_img;
image.copyTo(u_img);

gettimeofday(&start, NULL);
last_time = start;
for (int i = 0; i < times; i++) {
u_img.copyTo(out);
gettimeofday(&end, NULL);
P("mat.copyToUmat:%d,run times:%d, spend:%d us", i,times, (end.tv_sec - last_time.tv_sec) * 1000000 +
(end.tv_usec - last_time.tv_usec));
last_time = end;
}
gettimeofday(&end, NULL);
P("mat.copyToUmat: run times:%d, spend:%d ms", times, (end.tv_sec - start.tv_sec) * 1000 +
(end.tv_usec - start.tv_usec)/1000);
}

| 手机型号 | 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| 1920
1080 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| 720
480 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| 720
480 1579KB| 运行1000次,654M/S| 运行1000次,682M/s|

1.3 测试OpenCL cvtcolor函数性能

  在测试完CPU和GPU内存拷贝的性能之外,之后测试了cvtcolor函数的性能,由于动态加载,OpenCL函数首次加载时特别耗时,大概需要200ms。除此之外,在不同规格的图片上,OpenCL的计算性能大概是cpu的2到3倍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void cpu(const char* img, int times) {
cv::Mat image;
cv::Mat out;
struct timeval start, end,last;
for (int i = 0; i < times; i++) {
image = cv::imread(img, cv::IMREAD_UNCHANGED);
gettimeofday(&start, NULL);
cv::cvtColor(image, out, cv::COLOR_BGR2GRAY);
gettimeofday(&end, NULL);
P("run times:%d, spend:%d us", i, (end.tv_sec - start.tv_sec) * 1000000 +
(end.tv_usec - start.tv_usec));
}
}

void OpenCL(const char* img, int times) {
cv::UMat u_img;
cv::Mat image;
cv::UMat out;
cv::Mat out1;

std::vector<cv::UMat> v;
for(int i=0;i<times;i++){
image = cv::imread(img, cv::IMREAD_UNCHANGED);
cv::UMat u_img;
image.copyTo(u_img);
v.push_back(u_img);
}
struct timeval start, end,last;
for (int i = 0; i < times; i++) {
gettimeofday(&start, NULL);
cv::cvtColor(v[i], out, cv::COLOR_BGR2GRAY);
gettimeofday(&end, NULL);
P("run times:%d, spend:%d us", i, (end.tv_sec - start.tv_sec) * 1000000 +
(end.tv_usec - start.tv_usec));
}
}

测试数据:

手机型号 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

二、OpenCL核心API性能测试

手机型号 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数量与效率的关系

  在OpenCL编程中,work_item和work_group的设置对程序的性能有较大的影响。这里以内存拷贝为例测试OpenCL中work_item数量与效率的关系。通过一张3840x2160的图片拷贝,分别测试了CPU和GPU内存拷贝的性能,测试了在不同work_item条件下GPU内存拷贝性能的性能。从测试结果来看,不同work_item对opencl的性能有较大的影响。测试结果显示,最开始时work_item数量曾倍数关系,之后会在100ms抖动,最好的情况是work_item数量与bmpsize大小相同。测试机器为小米mix2s。

3.1循环拷贝

bmpsize = 3840x2160x3,运行时间13ms

1
2
3
4
5
char* out = new char[bmp_size];
for(int i=0;i<bmp_size;i++){
// P("%d",i);
out[i] = bmp_data[i];
}

3.2memcpy拷贝

bmpsize = 3840x2160x3,运行时间3ms

1
memcpy(out,bmp_data,bmp_size);

3.3 opencl拷贝

核函数:

1
2
3
4
5
6
7
8
9
10
11
12
__kernel void convert_image(__global const uchar* in, 
__global uchar* out,const int channel,
const int width, const int height){
int thread_count = get_global_size(0);
int size = width * height * channel;
int each_thread = size / thread_count;
int tid = get_global_id(0);
; out[tid] = in[tid];
for(int i=tid*each_thread;i<(tid+1)*each_thread;i++){
out[i] = in[i];
}
}

关键代码与work_item的设置:

1
2
3
4
5
6
7
P("thread_count=%d", thread_count);
gettimeofday(&start,NULL);
err = queue.enqueueNDRangeKernel(kernel, cl::NullRange, cl::NDRange(thread_count, 1),
cl::NullRange, NULL, &event);
event.wait();
gettimeofday(&end,NULL);
P("opecl wait:%d ms", (end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec)/1000);
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值效果会更好。

四、多commandqueue性能测试

  在学习和测试OpenCL的过程中,有一个疑问能否使用多个commandqueue来做任务的并行。假设有n个任务,每个任务包含CPU到GPU内存拷贝,核函数执行,和GPU到CPU的内存拷贝。分别测试了使用一个commandqueue和n个commandqueue的性能,测试结果显示多个commandqueue会比使用单个commandqueue性能略好一些,但是差别不大。除此之外,与work_item的设置有关,多个commandqueue可能比单个commandqueue性能性能提升15%。从GPU利用率来说,单个commandqueuGPU曲线呈锯齿形状,而多个commandque呈梯形。部分代码如下:
单个commandqueue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
void test(const char* cl_file, const char* name, 
const char* bmp_data, const int bmp_size,
const int width, const int height, const int channels,
const int line_size, const int thread_count,
const int run_times)
{
cl::Platform platforms = cl::Platform::getDefault();
//P("platform count:%d", platforms.size());
cl::Context context(CL_DEVICE_TYPE_GPU, NULL);
std::vector<cl::Device> devices = context.getInfo<CL_CONTEXT_DEVICES>();
P("Device count:%d", devices.size());
std::ifstream in(cl_file, std::ios::in);
std::stringstream buffer;
buffer << in.rdbuf();
cl_int err = CL_SUCCESS;
cl::Program program_ = cl::Program(context, buffer.str());
err = program_.build(devices);
if (err != CL_SUCCESS) {
P("build error");
return;
}
cl::Kernel kernel(program_, name, &err);
if (err != CL_SUCCESS) {
P("build error");
return;
}

cl::CommandQueue queue(context, devices[0], 0, &err);
if (err != CL_SUCCESS) {
P("CommandQueue create error");
return;
}

struct timeval start, end;
cl::Event event;
err = CL_SUCCESS;
for(int i = 0;i<run_times;i++){
{

//see: https://github.khronos.org/OpenCL-CLHPP/classcl_1_1_buffer.html
cl::Buffer in_buf(context, CL_MEM_WRITE_ONLY, bmp_size);
cl::Buffer out_buf(context, CL_MEM_READ_ONLY, bmp_size);
err = queue.enqueueWriteBuffer(in_buf, true, 0, bmp_size, bmp_data, NULL, &event);

kernel.setArg(0, in_buf);
kernel.setArg(1, out_buf);
kernel.setArg(2, line_size);
kernel.setArg(3, channels);
kernel.setArg(4, width);
kernel.setArg(5, height);

P("thread_count=%d", thread_count);
gettimeofday(&start,NULL);
err = queue.enqueueNDRangeKernel(kernel, cl::NullRange, cl::NDRange(thread_count, 1),
cl::NullRange, NULL, &event);
event.wait();
gettimeofday(&end,NULL);
P("opecl wait:%d ms", (end.tv_sec - start.tv_sec) * 1000 +
(end.tv_usec - start.tv_usec)/1000);


char* h_out_buf = new char[bmp_size];
err = queue.enqueueReadBuffer(out_buf, true, 0, bmp_size, h_out_buf, NULL, &event);
if(0!=memcmp(h_out_buf, bmp_data, bmp_size)){
P("data not same");
return;
}else{
P("data same");
}
}
}
}

多个commandqueue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
void test_mutil_command_queue(const char* cl_file, const char* name, 
const char* bmp_data, int bmp_size,
const int width, const int height, const int channels,
const int line_size, const int thread_count,
const int run_times)
{
cl::Platform platforms = cl::Platform::getDefault();
//P("platform count:%d", platforms.size());
cl::Context context(CL_DEVICE_TYPE_GPU, NULL);
std::vector<cl::Device> devices = context.getInfo<CL_CONTEXT_DEVICES>();
P("Device count:%d", devices.size());
// cl::CommandQueue queue(context, devices[0], 0);
//
std::ifstream in(cl_file, std::ios::in);
std::stringstream buffer;
buffer << in.rdbuf();
//
//cl::Program::Sources source{
// std::make_pair(buffer.str().c_str(), buffer.str().size()) };
cl_int err = CL_SUCCESS;
cl::Program program_ = cl::Program(context, buffer.str());
err = program_.build(devices);
if (err != CL_SUCCESS) {
P("build error %d",err);
return;
}

struct timeval start, end,end2;
cl::Event event;
err = CL_SUCCESS;
gettimeofday(&start, NULL);
std::vector<cl::CommandQueue> vQueue;
std::vector<cl::Event> vEvents;
std::vector<cl::Buffer> vInBuffers;
std::vector<cl::Buffer> vOutBuffers;
std::vector<char*> vHostOutBuf;
std::vector<cl::Kernel> vKernels;
std::vector<char*> vBmpdatas;

for(int i=0;i<run_times;i++){
cl::Event event;
cl::CommandQueue queue(context, devices[0], 0, &err);
if (err != CL_SUCCESS) {
P("CommandQueue create error");
return;
}
vQueue.push_back(queue);
vEvents.push_back(event);
cl::Buffer in_buf(context, CL_MEM_WRITE_ONLY, bmp_size);
cl::Buffer out_buf(context, CL_MEM_READ_ONLY, bmp_size);
vInBuffers.push_back(in_buf);
vOutBuffers.push_back(out_buf);
char* h_out_buf = new char[bmp_size];
vHostOutBuf.push_back(h_out_buf);

cl::Kernel kernel(program_, name, &err);
if (err != CL_SUCCESS) {
P("build error");
return;
}
kernel.setArg(0, vInBuffers[i]);
kernel.setArg(1, vOutBuffers[i]);
kernel.setArg(2, line_size);
kernel.setArg(3, channels);
kernel.setArg(4, width);
kernel.setArg(5, height);
vKernels.push_back(kernel);

}
gettimeofday(&end, NULL);
P("opecl create queue: spend:%d ms", (end.tv_sec - start.tv_sec) * 1000 +
(end.tv_usec - start.tv_usec)/1000);

for(int i=0;i<run_times;i++){
err = vQueue[i].enqueueWriteBuffer(vInBuffers[i], false, 0, bmp_size, bmp_data, NULL, &vEvents[i]);
}
for(int i=0;i<run_times;i++){
vEvents[i].wait();
}

for(int i=0;i<run_times;i++){
err = vQueue[i].enqueueNDRangeKernel(vKernels[i], cl::NullRange, cl::NDRange(thread_count, 1),
cl::NullRange, NULL, &vEvents[i]);
}

for (int i = 0; i < run_times; ++i){
vEvents[i].wait();
}

for(int i=0;i<run_times;i++){
err = vQueue[i].enqueueReadBuffer(vOutBuffers[i], false, 0, bmp_size, vHostOutBuf[i], NULL, &vEvents[i]);
}
for(int i=0;i<run_times;i++){
vEvents[i].wait();
}

for(int i=0;i<run_times;i++){
if (0!=memcmp(vHostOutBuf[i], bmp_data, bmp_size)){
P("data not same");
}else{
P("data same");
}
}
}

一、

今年是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。

一、benchmark_model模型推理速度分析

  在深度学习模型工程落地时,我们追求在成本可控的前提下提高良好的用户体验,因此模型的推理效率和计算代价是重要的衡量指标。通常用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
2
3
4
5
6
7
8
9
10
11
12
编译benchmark_model
$ bazel build -c opt tensorflow/tools/benchmark:benchmark_model
$ bazel-bin/tensorflow/tools/benchmark/benchmark_model \
--graph=model_original.pb \
--input_layer="net_input" \
--input_layer_shape="1,32,32,3" \
--input_layer_type="float" \
--output_layer="net_output" \
--show_flops=true \
--show_run_order=false \
--show_time=false \
--num_threads=1

预估FLOPs

1
2
2019-10-11 21:30:31.288678: I tensorflow/tools/benchmark/benchmark_model.cc:636] FLOPs estimate: 82.15M
2019-10-11 21:30:31.288744: I tensorflow/tools/benchmark/benchmark_model.cc:638] FLOPs/second: 21.89B

查看不同类型节点消耗的时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
========================= Summary by node type ==========================================
[Node type] [count] [avg ms] [avg %] [cdf %] [mem KB] [times called]
<> 65 4.110 47.269% 47.269% 0.000 65
FusedBatchNorm 19 2.028 23.324% 70.592% 240.384 19
Conv2D 22 2.003 23.036% 93.629% 868.352 22
Pad 2 0.239 2.749% 96.377% 115.456 2
Relu 19 0.082 0.943% 97.320% 0.000 19
Const 65 0.071 0.817% 98.137% 0.000 65
NoOp 1 0.066 0.759% 98.896% 0.000 1
Add 9 0.059 0.679% 99.574% 0.000 9
Mean 1 0.010 0.115% 99.689% 0.256 1
Softmax 1 0.008 0.092% 99.781% 0.000 1
_FusedMatMul 1 0.007 0.081% 99.862% 0.040 1
_Retval 1 0.005 0.058% 99.919% 0.000 1
Squeeze 1 0.005 0.058% 99.977% 0.000 1
_Arg 1 0.002 0.023% 100.000% 0.000 1

Timings (microseconds): count=1000 first=7287 curr=7567 min=7198 max=18864 avg=8794.03 std=1249
Memory (bytes): count=1000 curr=1224488(all same)
  • node type:进行操作的节点类型。
  • start:运算符的启动时间,展示了其在操作顺序中的位置。
  • first: 以毫秒为单位。默认情况下 TensorFlow 会执行 20 次运行结果来获得统计数据,这个字段则表示第一次运行基准测试所需的操作时间。
  • avg ms:以毫秒为单位。表示整个运行的平均操作时间。
  • %:一次运行占总运行时间的百分比。这对理解密集计算区域非常有用。
  • cdf%:整个过程中表格中当前运算符及上方全部运算符的累积计算时间。这对理解神经网络不同层之间的性能分布非常重要,有助于查看是否只有少数节点占用大部分时间。
  • mem KB:当前层消耗的内存大小。
  • Name:节点名称。

二、summarize_graph 模型大小分析

  服务端深度模型落地时主要关注模型的预测效率,移动端模型落地需要考虑模型的大小。通过summarize_graph工具可以帮助我们简要分析模型的参数量和包含哪些op。设置–print_structure=true可以观察到模型的结构,这也可以通过tensorboard来可视化实现。

1
2
3
4
5
6
tensorflow-1.14.0编译summarize_graph工具
$ bazel build -c opt tensorflow/tools/graph_transforms:summarize_graph
$ bazel-bin/tensorflow/tools/graph_transforms/summarize_graph \
--in_graph=reset20_cifar10_original.pb \
--print_structure=true

1
2
3
4
5
Found 1 possible inputs: (name=net_input, type=float(1), shape=[?,32,32,3]) 
No variables spotted.
Found 1 possible outputs: (name=net_output, op=Softmax)
Found 272572 (272.57k) const parameters, 0 (0) variable parameters, and 0 control_edges
Op types used: 194 Const, 77 Identity, 22 Conv2D, 19 Relu, 19 FusedBatchNorm, 11 Add, 6 Slice, 5 Pad, 5 Reshape, 4 Sub, 4 MatchingFiles, 3 Switch, 2 Squeeze, 2 ShuffleDataset, 2 ShuffleAndRepeatDataset, 2 StridedSlice, 2 Shape, 2 TensorSliceDataset, 2 RealDiv, 2 PrefetchDataset, 2 ParallelMapDataset, 2 ParallelInterleaveDataset, 2 Transpose, 2 OneHot, 2 BatchDatasetV2, 2 Cast, 2 Maximum, 2 DecodeRaw, 1 GreaterEqual, 1 All, 1 Assert, 1 BiasAdd, 1 Softmax, 1 ExpandDims, 1 FixedLengthRecordDataset, 1 FloorMod, 1 Mul, 1 ReverseV2, 1 Less, 1 MatMul, 1 RandomUniformInt, 1 RandomUniform, 1 Mean, 1 Placeholder, 1 Merge

https://tensorflow.juejin.im/mobile/optimizing.html

  tensorflow针对训练、预测、服务端和移动端等环境支持多种模型格式,这对于初学者来说可能比较疑惑。目前,tf中主要包括.ckpt格式、.pb格式SavedModel和tflite四种格式的模型文件。SavedModel用于tensorflow serving环境中,tflite格式模型文件用在移动端,后续遇到相关格式模型文件会继续补充。这里主要介绍常见的ckpt和pb格式的模型文件,以及它们之间的转换方法。

CheckPoint(*.ckpt)

  在使用tensorflow训练模型时,我们常常使用tf.train.Saver类保存和还原,使用该类保存和模型格式称为checkpoint格式。Saver类的save函数将图结构和变量值存在指定路径的三个文件中,restore方法从指定路径下恢复模型。当数据量和迭代次数很多时,训练常常需要数天才能完成,为了防止中间出现异常情况,checkpoint方式能帮助保存训练中间结果,避免重头开始训练的尴尬局面。有些地方说ckpt文件不包括图结构不能重建图是不对的,使用saver类可以保存模型中的全部信息。尽管ckpt模型格式对于训练时非常方便,但是对于预测却不是很好,主要有下面这几个缺点:

  1. ckpt格式的模型文件依赖于tensorflow,只能在该框架下使用;
  2. ckpt模型文件保存了模型的全部信息,但是在使用模型预测时,有些信息可能是不需要的。模型预测时,只需要模型的结构和参数变量的取值,因为预测和训练不同,预测不需要变量初始化、反向传播或者模型保存等辅助节点;
  3. ckpt将模型的变量值和计算图分开存储,变量值存在index和data文件中,计算图信息存储在meta文件中,这给模型存储会有一定的不方便。

frozen model(*.pb)

  Google推荐将模型保存为pb格式。PB文件本身就具有语言独立性,而且能被其它语言和深度学习框架读取和继续训练,所以PB文件是最佳的格式选择。另外相比ckpt格式的文件,pb格式可以去掉与预测无关的节点,单个模型文件也方便部署,因此实践中我们常常使用pb格式的模型文件。那么如何将ckpt格式的模型文件转化为pb的格式文件呢?主要包含下面几个步骤,结合这几个步骤写了个通用的脚本,使用该脚本只需指定ckpt模型路径、pb模型路径和模型的输出节点,多个输出节点时使用逗号隔开。

  • 通过传入的ckpt模型的路径得到模型的图和变量数据
  • 通过 import_meta_graph 导入模型中的图
  • 通过 saver.restore 从模型中恢复图中各个变量的数据
  • 通过 graph_util.convert_variables_to_constants 将模型持久化
  • 在frozen model的时候可以删除训练节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# -*-coding: utf-8 -*-
import tensorflow as tf
from tensorflow.python.framework import graph_util
import argparse


def freeze_graph(input_checkpoint,output_pb_path,output_nodes_name):
'''
:param input_checkpoint:
:param output_pb_path: PB模型保存路径
'''
saver = tf.train.import_meta_graph(input_checkpoint + '.meta', clear_devices=True)
with tf.Session() as sess:
saver.restore(sess, input_checkpoint) #恢复图并得到数据
graph = tf.get_default_graph()
# 模型持久化,将变量值固定
output_graph_def = graph_util.convert_variables_to_constants(
sess=sess,
input_graph_def=sess.graph_def,
output_node_names=output_nodes_name.split(","))# 如果有多个输出节点,以逗号隔开

print("++++++++++++++%d ops in the freeze graph." % len(output_graph_def.node)) #得到当前图有几个操作节点
output_graph_def = graph_util.remove_training_nodes(output_graph_def)
print("++++++++++++++%d ops after remove training nodes." % len(output_graph_def.node)) #得到当前图有几个操作节点

# serialize and write pb model to Specified path
with tf.gfile.GFile(output_pb_path, "wb") as f:
f.write(output_graph_def.SerializeToString())

if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--ckpt_path', type=str, required=True,help='checkpoint file path')
parser.add_argument('--pb_path', type=str, required=True,help='pb model file path')
parser.add_argument('--output_nodes_name', type=str, required=True,help='name of output nodes separated by comma')

args = parser.parse_args()
freeze_graph(args.ckpt_path,args.pb_path,args.output_nodes_name)

参考:
https://blog.metaflow.fr/tensorflow-how-to-freeze-a-model-and-serve-it-with-a-python-api-d4f3596b3adc

一、概述

  最近在做模型压缩(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 基本原理

1. 什么是通道剪枝

  虽然论文末尾谈到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中的某些通道剪掉,但是剪掉后的BW的卷积结果能尽可能和C接近。当删减B中的某些通道时,同时也裁剪了W中与这些通道的对应的卷积核,因此通过通过剪枝能减小卷积的运算量。

channel-pruning示意图

2. 通道剪枝数学描述

  通道剪枝的思想是简单的,难点是怎么选择要裁剪的通道,同时要保证输出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】。

channel-pruning示意图

三、PocketFlow

  PocketFlow是腾讯AI Lab开源的自动化深度学习模型压缩框架,它集成了腾讯自己研发的和来自其他同行的主流的模型压缩与训练算法,还引入了自研的超参数优化组件,实现了自动托管式模型压缩与加速。PocketFlow能够自动选择模型压缩的超参,极大的方便了算法人员的调参。这里主要使用里面的channel pruning算法(learner)进行通道剪枝。【4】

1.实验准备:

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

2.准备配置文件path.conf

1
2
3
4
5
6
7
# data files
data_dir_local_cifar10 = ./cifar-10-binary/cifar-10-batches-bin #cifar10数据集解压的位置

# model files
# 这里模型文件用wget下载不下来,要登录下载,解压到PocketFlow根目录的model目录下面
model_http_url = https://share.weiyun.com/5610f11d61dfb733db1f2c77a9f34531

3.在本地运行通道剪枝的learner

1
2
3
4
5
6
$ ./scripts/run_local.sh nets/resnet_at_cifar10_run.py \
--learner=channel \
--cp_uniform_preserve_ratio=0.5 \
--cp_prune_option=uniform \
--resnet_size=56

4. 模型转换

步骤3之后会在models产生ckpt文件,需要通过进行模型转化,最终会生成model_original.pb,model_transformed.pb,同时也会生成移动端对应的tflite文件。

1
2
3
4
$ python tools/conversion/export_chn_pruned_tflite_model.py \
--model_dir=models/pruned_model
--input_coll=train_images
--output_coll=logits

四、剪枝前后模型分析

  我们可以通过之前介绍的模型基准测试工具benchmark_model分别测试剪枝前后的模型。可以很清楚看到通道剪枝大大减少了模型前向计算的FLOPs的变化,以及各阶段、算子的耗时和内存消耗情况。可以发现模型下降为原来的1/2,卷积耗时下降接近50%。除此之外通过netron工具可以直观的看到模型通道剪枝前后结构发生的变化,通道剪枝之后的模型中明显增加了许多conv1*1的卷积。这里主要利用1x1卷积先降维,然后升维度,达到减少计算量的目的。1x1卷积还有多种用途,可以参考【5】。

1
2
3
4
5
6
7
8
9
10
11
$ bazel-bin/tensorflow/tools/benchmark/benchmark_model \ 
--graph=model_original.pb \
--input_layer="net_input" \
--input_layer_shape="1,32,32,3" \
--input_layer_type="float" \
--output_layer="net_output" \
--show_flops=true \
--show_run_order=false \
--show_time=true \
--num_threads=1

channel-pruning 1x1 convolution

参考:
[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 则不改变计算方式,可以直接使用,加速效果相对较好,之后也会继续尝试。

二、tensorflow/contrib/model_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} $$

  • initial_sparsity:初始稀疏值$s_i$
  • target_sparsity:目标稀疏值$s_f$
  • sparsity_function_begin_step:开始剪枝的step $t_0$
  • sparsity_function_end_step: 剪枝停止的step
  • pruning_frequency:剪枝的频率$\Delta t$,文章提出在100到1000之间通常比较好
  • sparsity_function_exponent: 剪枝函数的指数,表示式中已描述为默认的3,表示由快到慢,为1时表示线性剪枝

三、tensorflow中的model_pruning实践

  tensorflow中model_pruning的源码位于tensorflow/contrib/model_pruning。

  1. 准备tensorflow-1.14.0源码

  2. 编译model_pruning

    1
    $bazel build -c opt tensorflow/contrib/model_pruning/examples/cifar10:cifar10_train
  3. 通过设置一些参数,开始针对cifar10剪枝

    1
    2
    3
    4
    5
    6
    7
    $bazel-out/k8-py2-opt/bin/tensorflow/contrib/model_pruning/examples/cifar10/cifar10_train \
    --train_dir=/home/terse/code/programming/tensorflow/model_pruning/train \
    --pruning_hparams=name=cifar10_pruning,\
    initial_sparsity=0.3,\
    target_sparsity=0.9,\
    sparsity_function_begin_step=100,\
    sparsity_function_end_step=10000
  4. 可通过tensorboard查看剪枝过程。可以清楚的看出随着训练步骤的增加,conv1和conv2的sparsity在不断的增长。 在GRAPHS 页面,双击conv节点,可以看到在原有计算图基础上新增了mask和threshold节点用来做 model pruning

    1
    $tensorboard --logdir=/home/terse/code/programming/tensorflow/model_pruning/train
  5. 模型剪枝之后将剪枝的ops从训练图中删除。

    1
    2
    3
    4
    5
    6
    $bazel build -c opt tensorflow/contrib/model_pruning:strip_pruning_vars
    $bazel-out/k8-py2-opt/bin/tensorflow/contrib/model_pruning/strip_pruning_vars \
    --checkpoint_dir=/home/terse/code/programming/tensorflow/model_pruning/train \
    --output_node_names=softmax_linear/softmax_linear_2 \
    --output_dir=/home/terse/code/programming/tensorflow/model_pruning \
    --filename=pruning_stripped.pb

四、model_pruning源码简单分析

  使用tensorflow的model_pruning进行模型剪枝,主要包括两方面的工作,一是apply_mask,二是在训练图中增加剪枝的节点(pruning ops)。这里分别截取了其中的两段代码。

1
2
3
4
5
6
7
8
9
10
11
12
# cifar10_pruning.py  apply_mask to the graph
with tf.variable_scope('conv1') as scope:
kernel = _variable_with_weight_decay(
'weights', shape=[5, 5, 3, 64], stddev=5e-2, wd=0.0)

conv = tf.nn.conv2d(
images, pruning.apply_mask(kernel, scope), [1, 1, 1, 1], padding='SAME')

biases = _variable_on_cpu('biases', [64], tf.constant_initializer(0.0))
pre_activation = tf.nn.bias_add(conv, biases)
conv1 = tf.nn.relu(pre_activation, name=scope.name)
_activation_summary(conv1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 #Adding pruning ops to the training graph
with tf.graph.as_default():

# Create global step variable
global_step = tf.train.get_or_create_global_step()

# Parse pruning hyperparameters
pruning_hparams = pruning.get_pruning_hparams().parse(FLAGS.pruning_hparams)

# Create a pruning object using the pruning specification
p = pruning.Pruning(pruning_hparams, global_step=global_step)

# Add conditional mask update op. Executing this op will update all
# the masks in the graph if the current global step is in the range
# [begin_pruning_step, end_pruning_step] as specified by the pruning spec
mask_update_op = p.conditional_mask_update_op()

# Add summaries to keep track of the sparsity in different layers during training
p.add_pruning_summaries()

with tf.train.MonitoredTrainingSession(...) as mon_sess:
# Run the usual training op in the tf session
mon_sess.run(train_op)

# Update the masks by running the mask_update_op
mon_sess.run(mask_update_op)

五、总结和未解决的问题

  1. tensorflow中的模型剪枝属于no-structral,本质上是使权重稀疏化(weight sparsification),实践中发现它没有使推理加速,据其加速效果依赖具体的硬件实现,加速效果一般,tensorflow 中对稀疏矩阵运算没有特别好的优化(依赖于底层的 SparseBLAS 实现,目前还没有特别好的)
  2. 实践中发现不管稀疏度为多少,其剪枝后的模型大小都是相同的,是不是tensorflow对稀疏的模型也是按照非稀疏格式存储的?
  3. issue:model_pruning: Why 50% and 90% zeros of the stripped models are the same size? #32805
  4. issue: [CNN.Model pruning: no gain in speeding up of inference #22732](CNN.Model pruning: no gain in speeding up of inference #22732)

参考:

  1. https://github.com/tensorflow/tensorflow/tree/r2.0/tensorflow/contrib/model_pruning
  2. Michael Zhu and Suyog Gupta, “To prune, or not to prune: exploring the efficacy of pruning for model compression”, 2017 NIPS
  3. https://zhuanlan.zhihu.com/p/48069799

  
  最近在尝试深度学习模型加速的工作,查了一些资料,发现模型推理加速的研究还挺多的,主要从四个方面进行,从头开始构建轻量高效的模型,例如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,但在推理效率并没有提升,甚至略有下降

二、什么是量化

2.1 实数量化

  网络上关于模型量化的内容挺多的,量化本质上是一种仿射图(affine map),它以表达式(1)将实数值表示映射为量化的uint8,当然也可以等效为表示式(2):

1
2
real_value = A * quantized_value + B             (1) 
real_value = C * (quantized_value + D) (2)

  除此之外,深度学习模型量化中有一个约束条件,0必须准确的表示,不能有误差。因为对于某些神经网络层,实数0精确表示对于优化实现非常有用,例如在具有填充的卷积层或池化层中,长度对输入数组进行零填充(zero-padding)来实现填充是有用的。实数值0对应的量化值称之为零点(zero-point)。实际上,如果0不能完全表示,当我们用0对应的量化值进行填充时,因为这与实际值0不完全对应,会导致结果不准确,引入偏差。因此有:

1
2
3
4
5
  0=A∗zero_point+B
  zero_point=−B/A
  0=C∗(zero_point+D)
  0=zero_point+D
  D=−zero_point

  结合上述条件,可以得出量化的最终表达式为(3),它能做到0值的准确表示,zero_point是0对应的量化值。表示式(3)中有两个常量,zero_point是量化值,通常是uint8值,scale是一个正实数,通常为float32。
$$real\_value = scale * (quantized\_value - zero\_point)  (3)$$

2.2 矩阵乘法量化

  根据表达式(3),我们可以将实数值(通常为float32)用量化值(通常为uint8)表示,下面将介绍怎么把它应用到矩阵乘法当中。假设有两个实数矩阵$lhs\_real\_matrix, rhs\_real\_matrix$,量化之后就会有对应的$lhs\_scale, rhs\_scale, lhs\_zero\_point, rhs\_zero\_point$,矩阵中的实数值可以用其量化值表示为:

1
2
lhs_real_value[i] = lhs_scale * (lhs_quantized_value[i] - lhs_zero_point)
rhs_real_value[i] = rhs_scale * (rhs_quantized_value[i] - rhs_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
result_real_value
= Sum_over_i(lhs_real_value[i] * rhs_real_value[i])
= Sum_over_i(
lhs_scale * (lhs_quantized_value[i] - lhs_zero_point) *
rhs_scale * (rhs_quantized_value[i] - rhs_zero_point)
)
= lhs_scale * rhs_scale * Sum_over_i(
(lhs_quantized_value[i] - lhs_zero_point) *
(rhs_quantized_value[i] - rhs_zero_point)
) (4)

result_real_value = result_scale * (result_quantized_value - result_zero_point)
result_quantized_value = result_zero_point + result_real_value / result_scale (5)

result_quantized_value = result_zero_point +
(lhs_scale * rhs_scale / result_scale) *
Sum_over_i(
(lhs_quantized_value[i] - lhs_zero_point) *
(rhs_quantized_value[i] - rhs_zero_point)
) (6)

三、tensorflow模型量化方案

  **训练后量化(post training Quantization)**。在许多情况下,我们希望在不重新训练模型的前提下,只是通过压缩权重或量化权重和激活输出来缩减模型大小,从而加快预测速度。“训练后量化”就是这种使用简单,而且在有限的数据条件下就可以完成量化的技术。训练后量化操作简单,只需要使用量化工具将训练好的模型执行量化类型,即可实现模型的量化。训练后量化包括“只对权重量化”和“对权重和激活输出都量化”,对于很多网络而言,都可以产生和浮点型很接近的精度。

  **只对权重量化(weight only quantization)**。一个简单的方法是只将权重的精度从浮点型减低为8bit整型。由于只有权重进行量化,所以无需验证数据集就可以实现。一个简单的命令行工具就可以将权重从浮点型转换为8bit整型。如果只是想为了方便传输和存储而减小模型大小,而不考虑在预测时浮点型计算的性能开销的话,这种量化方法是很有用的。

  量化权重和激活输出(Quantizing weights and activations)。我们可以通过计算所有将要被量化的数据的量化参数,来将一个浮点型模型量化为一个8bit精度的整型模型。由于激活输出需要量化,这时我们就得需要标定数据了,并且需要计算激活输出的动态范围,一般使用100个小批量数据就足够估算出激活输出的动态范围了。

  **训练时量化(Quantization Aware Training)**。训练时量化方法相比于训练后量化,能够得到更高的精度。训练时量化方案可以利用Tensorflow的量化库,在训练和预测时在模型图中自动插入模拟量化操作来实现。由于训练时量化相对麻烦,加上权重量化没有实现加速的期望,所以没有尝试训练时量化,根据文档显示,其大概包括以下几个步骤:

  1. 可以在预训练好的模型基础上继续训练或者重新训练,建议在保存好的浮点型模型的基础上精调
  2. 修改估计器,添加量化运算,利用tf.contrib.quantize中的量化rewriter向模型中添加假的量化运算
  3. 训练模型,输出对于权重和激活输出都带有各自量化信息(尺度、零点)的模型
  4. 转换模型,利用tf.contrib.lite.toco convert定义的转换器,将带有量化参数的模型被转化成flatbuffer文件,该文件会将权重转换成int整型,同时包含了激活输出用于量化计算的信息
  5. 执行模型,转换后的带有整型权重的模型可以利用TFLite interpreter来执行,也可以在CPU上运行模型

四、tensorflow模型权重量化实验

  一开始尝试模型量化是因为有个复杂的视频分割模型推理效率很低,期望通过模型量化实现加速,在复杂模型上尝试失败之后,我用label_image的例子再次验证,结果显示也没有加速的效果。这里主要试验了训练后量化,尝试了只对权重量化和权重和激活量化,发现后者比前者性能更差,这里描述权重量化的过程。整个过程是比较简单的,tensorflow有两种量化方式,推荐使用第二种,编译命令行工具进行量化。

  1. 在tensorflow r1.0的版本中有个量化的脚本可以提供量化的功能:

    1
    2
    3
    4
    5
    6
    7
    8
    $wget "https://storage.googleapis.com/download.tensorflow.org/models/inception_v3_2016_08_28_frozen.pb.tar.gz"
    $tar -xzf tensorflow/examples/label_image/data
    $ work_dir=/home/terse/code/programming/tensorflow/quantization
    $ python tensorflow/tools/quantization/quantize_graph.py \
    --input=$work_dir/inception_v3_2016_08_28_frozen.pb \
    --output=$work_dir/inception_quantized0.pb \
    --output_node_names=InceptionV3/Predictions/Reshape_1 \
    --mode=weights
  2. 在较新版本的tf中,quantize_graph.py量化的脚本已经废弃了需要编译tensorflow的源码生成

    1
    2
    3
    4
    5
    6
    7
    tensorflow-1.14.0编译transform_graph工具
    $ bazel build tensorflow/tools/graph_transforms:transform_graph
    $ bazel-bin/tensorflow/tools/graph_transforms/transform_graph \
    --in_graph=$work_dir/inception_v3_2016_08_28_frozen.pb \
    --out_graph=$work_dir/inception_quantized1.pb \
    --outputs=InceptionV3/Predictions/Reshape_1 \
    --transforms='quantize_weights'
  3. 使用summarize_graph分析量化前后的模型区别,权重量化、模型减小、增加了一些和量化和反量化的节点。

    1
    2
    3
    4
    5
    tensorflow-1.14.0编译transform_graph工具
    $ bazel build tensorflow/tools/graph_transforms:summarize_graph
    $ bazel-bin/tensorflow/tools/graph_transforms/summarize_graph \
    --in_graph=$work_dir/inception_quantized1.pb \
    --print_structure=true
  4. 使用权重量化的模型做推理验证

    1
    2
    3
    4
    5
    $ bazel build tensorflow/examples/label_image:label_image
    $ bazel-bin/tensorflow/examples/label_image/label_image \
    --image=$work_dir/grace_hopper.jpg \
    --labels=$work_dir/imagenet_slim_labels.txt \
    --graph=$work_dir/inception_quantized1.pb

五、 为什么模型量化没有使推理加速

  关于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.

参考:

  1. https://zhuanlan.zhihu.com/p/33535898
  2. https://arxiv.org/abs/1806.08342
  3. https://github.com/google/gemmlowp/blob/master/doc/quantization.md
  4. https://github.com/tensorflow/tensorflow/issues/2807

代码生成的接口

  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())

relay图编译

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代码生成流程

Codegen的实现

TVM针对不同的target实现了许多的codgen,它完成了tir到目标代码的翻译工作,例如c,llvm ir等。我们也可以根据需求实现自己的codegen,官网提供了一个教程

  • target.build.c
  • target.build.llvm
  • target.build.cuda
  • target.build.opencl
  • target.build.opengl
  • target.build.metal
  • target.build.vulkan

References

[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。

为什么选择TVM

  为提升深度学习模型的推理效率,设备平台制造商针对自己的平台推出优化的推理引擎,例如NAVIDA的tensorRT,Intel的OpenVINO,Tencent针对移动端应用推出NCNN等。目前,深度学习模型应用广泛,在服务端和移动端都有应用,甚至于特殊的嵌入式场景想,它们都有加速模型推理的需求。个人感觉针对不同平台选择不同的推理引擎,学习成本太高。我这里选择尝试TVM,主要有以下几个原因:

  • 尝试了过一些模型压缩方法,效率提升有限
  • 有些是模型压缩方法需要推理引擎和硬件的支持的,例如量化
  • tensorflow推理效率有限,需要更好的推理引擎
  • 针对平台选择不同推理引擎,学习成本太高
  • 需要能支持跨平台的推理引擎,未来可能在定制的嵌入式芯片上运行深度学习模型
  • 除了TVM之外,还存在XLA之类方案,选择TVM也是因为tianqi等大佬主导的项目,相信大佬!

初次体验TVM,相比于tensorflow2倍的性能提升

  看了几篇TVM介绍文章后,了解到它是从深度学习编译器的角度来做推理引擎,目前技术领域还比较新,具体技术细节以后有机会会深入学习,这里主要想体验一下使用TVM做深度模型推理,重点是推理效率的提升,因为是骡子还是马得拉出来遛遛。参考官方文档进行编译安装,整个过程还是比较简单的,结果显示相比于tensorflow大概100%的性能提升。实验环境是ubuntu 19.04,x86_64架构。

  1. 安装llvm,也可源码编译
    1
    $ sudo apt-get install llvm
  2. 编译TVM
    1
    2
    3
    4
    5
    6
    7
    $ git clone --recursive https://github.com/dmlc/tvm.git
    $ cd tvm $$ mkdir build
    $ cp cmake/config.cmake build
    # 编辑config.cmake 然后将USE_LLVM OFF 改为 set(USE_LLVM /usr/bin/llvm-config)
    $ cd build
    $ cmake ..
    $ cmake -j4
  3. 编辑.bashrc配置Python环境
    1
    2
    export TVM_HOME=/home/xxxx/code/tvm
    export PYTHONPATH=$TVM_HOME/python:$TVM_HOME/topi/python:$TVM_HOME/nnvm/python
  4. 官方Compile Tensorflow Models
    直接运行出现了两个问题,下载文件时和SSL相关,另外一个是缺少antlr
    1
    2
    3
    4
    5
    6
    7
    # install antlr
    $ pip install antlr4-python3-runtime
    # debug ssl
    import ssl
    ssl._create_default_https_context = ssl._create_unverified_context
    # run demo
    $ python from_tensorflow.py
  5. 在代码中加入时间测试,实验测试结果。TVM与测试时间为0.277s,tensorflow为0.586s。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    ============ TVM ============ 0.2770531177520752
    African elephant, Loxodonta africana (score = 0.58335)
    tusker (score = 0.33901)
    Indian elephant, Elephas maximus (score = 0.02391)
    banana (score = 0.00025)
    vault (score = 0.00021)
    ============= Tensorflow ===== 0.58619508743286133
    ===== TENSORFLOW RESULTS =======
    African elephant, Loxodonta africana (score = 0.58394)
    tusker (score = 0.33909)
    Indian elephant, Elephas maximus (score = 0.03186)
    banana (score = 0.00022)
    desk (score = 0.00019)

未填的坑

  过程遇到一个坑,查了TVM社区,没有很好的解答,看起来好像会和性能有关,希望路过的大佬能帮忙解决。https://discuss.tvm.ai/t/cannot-find-config-for-target-llvm-when-using-autotvm-in-tensorflow-example-for-cpu/1544

1
2
3
4
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.
WARNING:autotvm:Cannot find config for target=llvm, workload=('conv2d', (1, 8, 8, 2048, 'float32'), (1, 1, 2048, 448, 'float32'), (1, 1), (0, 0), (1, 1), 'NHWC', 'float32'). A fallback configuration is used, which may bring great performance regression.
WARNING:autotvm:Cannot find config for target=llvm, workload=('conv2d', (1, 8, 8, 2048, 'float32'), (1, 1, 2048, 192, 'float32'), (1, 1), (0, 0), (1, 1), 'NHWC', 'float32'). A fallback configuration is used, which may bring great performance regression.

参考:

  1. tvm install: https://docs.tvm.ai/install/from_source.html
  2. tvm tutorial: Compile Tensorflow Models
  3. 未填的坑:https://discuss.tvm.ai/t/cannot-find-config-for-target-llvm-when-using-autotvm-in-tensorflow-example-for-cpu/1544

  坚持了接近一年的视频算法相关的项目,老板最终还是喊停了。并没有感到特别意外,只是在对一个东西突然有些兴趣或者说入门的时候,戛然而止,多少有些不甘心和遗憾,但是以后会在业余继续学习的,也希望自己在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.TVM编译和安装

2.TVM中向量相加

3.TVM编译tensorflow模型

4.TVM怎么做模型量化?(doing)

参考:
【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的运行过程。

SiameseRPN模型

  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。

SiameseRPN模型

  因此设模板分支输入为$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做视频目标追踪,DaSiamRPN相比SiameseRPN做了进一步的优化,例如训练时引入采样策略控制不平衡的样本分布,设计了一种distractor-aware模块执行增量学习等。结合官方的https://github.com/foolwood/DaSiamRPN 中的例子,很容易将demo运行起来。需要注意的是github上的代码需要gpu运行环境,如果要在无gpu机器上运行DaSiamRPN的demo需要将有关cuda代码去掉。例如将将net.eval().cuda()换成net.eval()。DaSiamRPN的运行包含两个步骤:

  1. 初始化。输入模板帧,得到$[ψ(z)]{cls}$和$[ψ(z)]{res}$两个用于卷积的核。
  2. 追踪。将待追踪帧输入到模型,得到每个候选区域的score和偏移delta。从候选区域中选出分数最高的候选区域proposal。

初始化

  1. 输出模板图片im,模板图片中目标位置target_pos,目标大小target_size,使用get_subwindow_tracking函数裁剪目标区域临近部分(target patch),并将裁剪得到图片resize到宽和高为127的图片。
  2. 将模板目标区域裁剪好的视频输入网络模型的模板分支(template branch),得到$[ψ(z)]{cls}$和$[ψ(z)]{res}$
  3. 使用generate_anchor函数产生anchers,其大小为$(271-127)/8+1=19,19*19*5=1805$,anchor的维度为(4,1805),这表示会有1805个候选区域,偏移量$d_x,d_y,d_w,d_h$

追踪

  1. 输入追踪的图片im,基于上一帧的target_pos和目标的大小位置target_size,在图片中裁剪部分区域并将该区域resize到271*271得到x_crop。
  2. 将x_crop输入网络的检测分支(detection branch)得到对所有anchor进行分类和回归得到delta和score。
  3. 根据delta获取细化后的候选区域(refinement coordinates)
    1
    2
    3
    4
    5
    # generate the refined top K proposals
    delta[0, :] = delta[0, :] * p.anchor[:, 2] + p.anchor[:, 0] #x
    delta[1, :] = delta[1, :] * p.anchor[:, 3] + p.anchor[:, 1] #y
    delta[2, :] = np.exp(delta[2, :]) * p.anchor[:, 2] #w
    delta[3, :] = np.exp(delta[3, :]) * p.anchor[:, 3] #h
  4. 结合scale penalty、ration penalty、cosine window调整每个候选区域score中每个候选区域的分数,选出分数最大的候选区域best_pscore_id.
    1
    2
    3
    4
    5
    6
    7
    8
    # size penalty
    s_c = change(sz(delta[2, :], delta[3, :]) / sz_wh(target_sz)) # scale penalty
    r_c = change((target_sz[0] / target_sz[1]) / (delta[2, :] / delta[3, :])) # ratio penalty
    penalty = np.exp(-(r_c * s_c - 1.) * p.penalty_k)
    pscore = penalty * score
    # window float
    pscore = pscore * (1 - p.window_influence) + window * p.window_influence
    best_pscore_id = np.argmax(pscore)
  5. 计算出当前帧目标的位置target_pos和target_size。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    target = delta[:, best_pscore_id] / scale_z
    target_sz = target_sz / scale_z

    lr = penalty[best_pscore_id] * score[best_pscore_id] * p.lr

    res_x = target_pos[0] + target[0]
    res_y = target_pos[1] + target[1]
    res_w = target_sz[0] * (1 - lr) + target[2] * lr
    res_h = target_sz[1] * (1 - lr) + target[3] * lr

    target_pos = np.array([res_x, res_y])
    target_sz = np.array([res_w, res_h])

参考:

  1. https://zhuanlan.zhihu.com/p/37856765
  2. https://github.com/foolwood/DaSiamRPN
  3. http://openaccess.thecvf.com/content_cvpr_2018/papers/Li_High_Performance_Visual_CVPR_2018_paper.pdf

  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

一、Hello Relay

既然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定义卷积单元

在学习Relay的时候参考了https://zhuanlan.zhihu.com/p/91283238 这篇文章。但是可能因为版本的问题,很多API多不兼容了,因此修改了一些地方,建议读者也可以去看一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import tvm
from tvm.relay import transform
import tvm.relay as relay
import numpy as np
from tvm.contrib import graph_runtime


def batch_norm_infer(data,
gamma=None,
beta=None,
moving_mean=None,
moving_var=None,
**kwargs):
name = kwargs.get("name")
kwargs.pop("name")
if not gamma:
gamma = relay.var(name + "_gamma")
if not beta:
beta = relay.var(name + "_beta")
if not moving_mean:
moving_mean = relay.var(name + "_moving_mean")
if not moving_var:
moving_var = relay.var(name + "_moving_var")
return relay.nn.batch_norm(data,
gamma=gamma,
beta=beta,
moving_mean=moving_mean,
moving_var=moving_var,
**kwargs)[0]

def conv2d(data, weight=None, **kwargs):
name = kwargs.get("name")
kwargs.pop("name")
if not weight:
weight = relay.var(name + "_weight")
return relay.nn.conv2d(data, weight, **kwargs)


def conv_block(data, name, channels, kernel_size=(3, 3), strides=(1, 1),
padding=(1, 1), epsilon=1e-5):
conv = conv2d(
data=data,
channels=channels,
kernel_size=kernel_size,
strides=strides,
padding=padding,
data_layout='NCHW',
name=name+'_conv')
bn = batch_norm_infer(data=conv, epsilon=epsilon, name=name + '_bn')
act = relay.nn.relu(data=bn)
return act


data_shape = (1, 3, 224, 224)
kernel_shape = (32, 3, 3, 3)
dtype = "float32"
data = relay.var("data", shape=data_shape, dtype=dtype)
act = conv_block(data, "graph", 32, strides=(2, 2))
func = relay.Function(relay.analysis.free_vars(act),act)


mod = relay.Module.from_expr(func)
mod = relay.transform.InferType()(mod)
shape_dict = {
v.name_hint : v.checked_type for v in mod["main"].params}
np.random.seed(0)
params = {}
for k, v in shape_dict.items():
if k == "data":
continue
init_value = np.random.uniform(-1, 1, v.concrete_shape).astype(v.dtype)
params[k] = tvm.nd.array(init_value, ctx=tvm.cpu(0))

target = "llvm"
ctx = tvm.context(target, 0)
print("Relay module function:\n", mod.astext(show_meta_data=False))
print("TVM parameters:\n", params.keys())

with relay.build_config(opt_level=3):
graph, lib, params = relay.build(mod, target, params=params)

print("TVM graph:\n", graph)
print("TVM parameters:\n", params.keys())
# print("TVM compiled target function:\n", lib.get_source())
module = graph_runtime.create(graph, lib, ctx)
data_tvm = tvm.nd.array((np.random.uniform(-1, 1, size=data_shape)).astype(dtype))
module.set_input('data', data_tvm)
module.set_input(**params)
module.run()
output = module.get_output(0)

三、Relay Graph Optimization

前面两个例子介绍了怎么使用relay构建网络,这个部分介绍怎么使用relay做图优化。上面例子代码中没有直接图优化的代码,而是包含在relay.build中。通过追踪代码,我们这部分的逻辑集中在 https://github.com/apache/incubator-tvm/blob/v0.6/src/relay/backend/build_module.cc 这个文件的optimize函数中。这里罗列了代码用到的pass,relay提供了方便的的文本形式中间描述,感兴趣的可以自己试一下每个pass之后,发生了哪些变化。

  • relay::qnn::transform::Legalize()),这个pass和qnn有关
  • transform::Legalize(),我理解的这个是和目标有关的优化,一个表达式虽然在语义上等效于另一个,但可以在目标上具有更好的性能。这个在需要在异构环境下生效。
  • transform::SimplifyInference() 。
    简化推理阶段的数据流图。在语义上等于输入表达式的简化表达式将被返回。例如将BatchNorm展开以及去掉 dropout。
  • transform::EliminateCommonSubexpr(fskip)),去除公共子表达式。
  • transform::CombineParallelConv2D(3),将多个conv2d运算符合并为一个,这部分优化会将具有相同输入的卷积合并成一个大的卷积运算。
  • transform::CombineParallelDense(3)),将多个dense运算符组合为一个
  • transform::FoldConstant(),常量传播优化。
  • transform::FoldScaleAxis()
  • transform::CanonicalizeCast(),
    将特殊运算符规范化为基本运算符。这样可以简化后续分析,例如将bias_add扩展为expand_dims和broadcast_add
  • transform::CanonicalizeOps()
  • transform::AlterOpLayout(),layout 变换
  • transform::FuseOps(),算子融合,根据一些规则,将expr中的运算符融合为较大的运算符。

四、使用Python API Relay 图优化

TVM核心代码是采用C++编写的,但是也提供了Python接口,这方面初学者体验的使用。Relay图优化核心功能都提供了对应的API,因此可以尝试一下,非常简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def my_optimize(func,params=None):

if params:
graph = _bind_params(func, params)

# https://docs.tvm.ai/api/python/relay/transform.html
optimize = relay.transform.Sequential([relay.transform.SimplifyInference(),
relay.transform.FoldConstant(),
relay.transform.FoldScaleAxis(),
relay.transform.CanonicalizeOps(),
relay.transform.FoldConstant()])

mod = relay.Module.from_expr(graph)
mod = optimize(mod)
return mod["main"]

mod['main'] = my_optimize(mod['main'], params)
print("Relay module function:\n", mod.astext(show_meta_data=False))

这里可以对比优化前后的IR.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Relay module function:
v0.0.4
def @main(%data: Tensor[(1, 3, 224, 224), float32], %graph_conv_weight: Tensor[(32, 3, 3, 3), float32], %graph_bn_gamma: Tensor[(32), float32], %graph_bn_beta: Tensor[(32), float32], %graph_bn_moving_mean: Tensor[(32), float32], %graph_bn_moving_var: Tensor[(32), float32]) -> Tensor[(1, 32, 112, 112), float32] {
%0 = nn.conv2d(%data, %graph_conv_weight, strides=[2, 2], padding=[1, 1], channels=32, kernel_size=[3, 3]) /* ty=Tensor[(1, 32, 112, 112), float32] */;
%1 = nn.batch_norm(%0, %graph_bn_gamma, %graph_bn_beta, %graph_bn_moving_mean, %graph_bn_moving_var) /* ty=(Tensor[(1, 32, 112, 112), float32], Tensor[(32), float32], Tensor[(32), float32]) */;
%2 = %1.0;
nn.relu(%2) /* ty=Tensor[(1, 32, 112, 112), float32] */
}
# =====================================
Relay module function:
v0.0.4
def @main(%data: Tensor[(1, 3, 224, 224), float32]) -> Tensor[(1, 32, 112, 112), float32] {
%0 = nn.conv2d(%data, meta[relay.Constant][0] /* ty=Tensor[(32, 3, 3, 3), float32] */ /* ty=Tensor[(32, 3, 3, 3), float32] */, strides=[2, 2], padding=[1, 1], channels=32, kernel_size=[3, 3]) /* ty=Tensor[(1, 32, 112, 112), float32] */;
%1 = multiply(%0, meta[relay.Constant][1] /* ty=Tensor[(32, 1, 1), float32] */ /* ty=Tensor[(32, 1, 1), float32] */) /* ty=Tensor[(1, 32, 112, 112), float32] */;
%2 = add(%1, meta[relay.Constant][2] /* ty=Tensor[(32, 1, 1), float32] */ /* ty=Tensor[(32, 1, 1), float32] */) /* ty=Tensor[(1, 32, 112, 112), float32] */;
nn.relu(%2) /* ty=Tensor[(1, 32, 112, 112), float32] */
}

// meta data omitted. you can use show_meta_data=True to include meta data

参考与进阶学习:
[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相当的效果,重要的是它能大大简化工作量。参考了一些文章,这里简单罗列了几个知识点和测试数据。

  1. 怎么评估硬件的理论性能?浮点峰值?
  2. 简单测试一下numpy的性能数据,硬件利用率
  3. 怎么做GEMM优化?
  4. 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 GEMM硬件利用率

  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?

  通用矩阵乘(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优化与numpy性能比较

  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年提出,将系统稳定性需求按照基础程度进行了不同层次的体系化区分,形成稳定性标准金字塔模型:

  • 金字塔的底座是监控(Monitoring),这是一个系统对于稳定性最基础的要求,缺少监控的系统,如同蒙上眼睛狂奔的野马,无从谈及可控性,更遑论稳定性。
  • 更上层是应急响应(Incident Response),从一个问题被监控发现到最终解决,这期间的耗时直接取决于应急响应机制的成熟度。合理的应急策略能保证当故障发生时,所有问题能得到有序且妥善的处理,而不是慌乱成一锅粥。
  • 事后总结以及根因分析(Postmortem&Root Caue Analysis),即我们平时谈到的“复盘”,虽然很多人都不太喜欢这项活动,但是不得不承认这是避免我们下次犯同样错误的最有效手段,只有当摸清故障的根因以及对应的缺陷,我们才能对症下药,合理进行规避。
  • 测试和发布管控(Testing&Release procedures),大大小小的应用都离不开不断的变更与发布,有效的测试与发布策略能保障系统所有新增变量都处于可控稳定区间内,从而达到整体服务终态稳定
  • 容量规划(Capacity Planning)则是针对于这方面变化进行的保障策略。现有系统体量是否足够支撑新的流量需求,整体链路上是否存在不对等的薄弱节点,都是容量规划需要考虑的问题。
  • 位于金字塔模型最顶端的是产品设计(Product)与软件研发(Development),即通过优秀的产品设计与软件设计使系统具备更高的可靠性,构建高可用产品架构体系,从而提升用户体验

系统稳定性建设概述


从金字塔模型我们可以看到构建维护一个高可用服务所需要做到的几方面工作:

  • 产品、技术、架构的设计,高可用的架构体系
  • 系统链路&业务策略梳理和维护(System & Biz Profiling)
  • 容量规划(Capacity Planning)
  • 应急响应(Incident Response)
  • 测试
  • 事后总结(Testing & Postmortem)
  • 监控(Monitoring)
  • 资损体系
  • 风控体系
  • 大促保障
  • 性能优化


高可用的架构设计

系统链路梳理和维护 System & Biz Profiling

系统链路梳理是所有保障工作的基础,如同对整体应用系统进行一次全面体检,从流量入口开始,按照链路轨迹,逐级分层节点,得到系统全局画像与核心保障点。

入口梳理盘点

一个系统往往存在十几个甚至更多流量入口,包含HTTP、RPC、消息等都多种来源。如果无法覆盖所有所有链路,可以从以下三类入口开始进行梳理:

  • 核心重保流量入口
    • 用户承诺服务SLI较高,对数据准确性、服务响应时间、可靠度具有明确要求。
    • 业务核心链路,浏览、下单、支付、履约
    • 面向企业级用户
  • 资损事件对应入口
    • 关联到公司资金收入或者客户资金收入收费服务
  • 大流量入口
    • 系统TPS&QPS TOP5~10
    • 该类入口虽然不涉及较高SLI与资损要求,但是流量较高,对整体系统负载有较大影响

节点分层判断

对于复杂场景可以做节点分层判断

流量入口就如同线团中的线头,挑出线头后就可按照流量轨迹对链路上的节点(HSF\DB\Tair\HBase等一切外部依赖)按照依赖程度、可用性、可靠性进行初级分层区分。

  1. 强弱依赖节点判断
  • 若节点不可用,链路业务逻辑被中断 or 高级别有损(存在一定耐受阈值),则为业务强依赖;反之为弱依赖。
  • 若节点不可用,链路执行逻辑被中断(return error),则为系统强依赖;反之为弱依赖。
  • 若节点不可用,系统性能受影响,则为系统强依赖;反之为弱依赖。
  • 按照快速失败设计逻辑,该类节点不应存在,但是在不变更应用代码前提下,如果出现该类节点,应作为强依赖看待。
  • 若节点无感可降级 or 存在业务轻微损伤替换方案,则为弱依赖。
  1. 低可用依赖节点判断
  • 节点服务日常超时严重
  • 节点对应系统资源不足
  1. 高风险节点判断
  • 上次大促后,节点存在大版本系统改造
  • 新上线未经历过大促的节点
  • 节点对应系统是否曾经出现高级别故障
  • 节点故障后存在资损风险

应产出数据

  • 识别核心接口(流程)调用拓扑图或者时序图(借用分布式链路追踪系统获得调用拓扑图)
  • 调用比
  • 识别资损风险
  • 识别内外部依赖

完成该项梳理工作后,我们应该产出以下数据:对应业务域所有核心链路分析,技术&业务强依赖、核心上游、下游系统、资损风险应明确标注。

监控&告警梳理 – Monitoring

站在监控的角度看,我们的系统从上到下一般可以分为三层:业务(Biz)、应用(Application)、系统(System)。系统层为最下层基础,表示操作系统相关状态;应用层为JVM层,涵盖主应用进程与中间件运行状态;业务层为最上层,为业务视角下服务对外运行状态。因此进行大促稳定性监控梳理时,可以先脱离现有监控,先从核心、资损链路开始,按照业务、应用(中间件、JVM、DB)、系统三个层次梳理需要哪些监控,再从根据这些索引找到对应的监控告警,如果不存在,则相应补上;如果存在则检查阈值、时间、告警人是否合理。

监控

监控系统一般有四项黄金指标:延时(Latency), 错误(Error),流量(Traffic), 饱和度(Situation),各层的关键性监控同样也可以按照这四项指标来进行归类,具体如下:


告警

是不是每项监控都需要告警?答案当然是否定的。建议优先设置Biz层告警,因为Biz层我们对外服务最直观业务表现,最贴切用户感受。Application&System层指标主要用于监控,部分关键&高风险指标可设置告警,用于问题排查定位以及故障提前发现。对于一项告警,我们一般需要关注级别、阈值、通知人等几个点。

  1. 级别
    即当前告警被触发时,问题的严重程度,一般来说有几个衡量点:
  • 是否关联NOC
  • 是否产生严重业务影响
  • 是否产生资损
  1. 阈值
  • 即一项告警的触发条件&时间,需根据具体场景合理制定。一般遵循以下原则:
  • 不可过于迟钝。一个合理的监控体系中,任何异常发生后都应触发相关告警。
  • 不可过于敏感。过于敏感的阈值会造成频繁告警,从而导致响应人员疲劳应对,无法筛选真实异常。若一个告警频繁出现,一般是两个原因:系统设计不合理 or 阈值设置不合理。
  • 若单一指标无法反馈覆盖整体业务场景,可结合多项指标关联构建。
  • 需符合业务波动曲线,不同时段可设置不同条件&通知策略。
  1. 通知人&方式
  • 若为业务指标异常(Biz层告警),通知人应为问题处理人员(开发、运维同学)与业务关注人员(TL、业务同学)的集合,通知方式较为实时,比如电话通知。
  • 若为应用 & 系统层告警,主要用于定位异常原因,通知人设置问题排查处理人员即可,通知方式可考虑钉钉、短信等低干扰方式。
  • 除了关联层次,对于不同级别的告警,通知人范围也可适当扩大,尤其是关联GOC故障的告警指标,应适当放宽范围,通知方式也应更为实时直接

应产出数据

完成该项梳理工作后,我们应该产出以下数据:

  1. 系统监控模型,格式同表1
  • Biz、Application、System 分别存在哪些待监控点
  • 监控点是否已全部存在指标,仍有哪些待补充
  1. 系统告警模型列表,需包含以下数据
  • 关联监控指标(链接)
  • 告警关键级别
  • 是否推送GOC
  • 是否产生资损
  • 是否关联故障
  • 是否关联预案
  1. 业务指标大盘,包含Biz层重点监控指标数据
  2. 系统&应用指标大盘,包含核心系统关键系统指标,可用于白盒监控定位问题。

业务策略&容量规划 Capacity Planning - 容量规划

业务策略

不同于高可用系统建设体系,大促稳定性保障体系与面向特定业务活动的针对性保障建设,因此,业务策略与数据是我们进行保障前不可或缺的数据。
一般大促业务数据可分为两类,全局业务形态评估以及应急策略&玩法。

全局评估

该类数据从可以帮助我们进行精准流量评估、峰值预测、大促人力排班等等,一般包含下面几类:

  • 业务量预估体量(日常X倍)
  • 预估峰值日期
  • 大促业务时长(XX日-XX日)
  • 业务场景预估流量分配

应急策略

  • 该类数据指相较于往年大促活动,本次大促业务变量,可用于应急响应预案与高风险节点评估等,一般包含下面两类:
  • 特殊业务玩法

容量规划的本质是追求计算风险最小化和计算成本最小化之间的平衡,只追求任意其一都不是合理的。为了达到这两者的最佳平衡点,需尽量精准计算系统峰值负载流量,再将流量根据单点资源负载上限换算成相应容量,得到最终容量规划模型。

流量模型评估

  1. 入口流量

对于一次大促,系统峰值入口流量一般由常规业务流量与非常规增量(比如容灾预案&业务营销策略变化带来的流量模型配比变化)叠加拟合而成。

  • 常规业务流量一般有两类计算方式:
    • 历史流量算法:该类算法假设当年大促增幅完全符合历史流量模型,根据当前&历年日常流量,计算整体业务体量同比增量模型;然后根据历年大促-日常对比,计算预估流量环比增量模型;最后二者拟合得到最终评估数据。
    • 由于计算时无需依赖任何业务信息输入,该类算法可用于保障工作初期业务尚未给出业务总量评估时使用,得到初估业务流量。
    • 业务量-流量转化算法(GMV\DAU\订单量):该类算法一般以业务预估总量(GMV\DAU\订单量)为输入,根据历史大促&日常业务量-流量转化模型(比如经典漏洞模型)换算得到对应子域业务体量评估。- 该种方式强依赖业务总量预估,可在保障工作中后期使用,在初估业务流量基础上纳入业务评估因素考虑。
  • 非常规增量一般指前台业务营销策略变更或系统应急预案执行后流量模型变化造成的增量流量。例如,NA61机房故障时,流量100%切换到NA62后,带来的增量变化.考虑到成本最小化,非常规增量P计算时一般无需与常规业务流量W一起,全量纳入叠加入口流量K,一般会将非常规策略发生概率λ作为权重
  1. 节点流量
    节点流量由入口流量根据流量分支模型,按比例转化而来。分支流量模型以系统链路为计算基础,遵循以下原则:
  • 同一入口,不同链路占比流量独立计算。
  • 针对同一链路上同一节点,若存在多次调用,需计算按倍数同比放大(比如DB\Tair等)。
  • DB写流量重点关注,可能出现热点造成DB HANG死。

容量转化

节点容量是指一个节点在运行过程中,能够同时处理的最大请求数。它反映了系统的瞬时负载能力。

1)Little Law衍生法则
不同类型资源节点(应用容器、Tair、DB、HBASE等)流量-容量转化比各不相同,但都服从Little Law衍生法则,即:
节点容量=节点吞吐率×平均响应时间

2)N + X 冗余原则

在满足目标流量所需要的最小容量基础上,冗余保留X单位冗余能力
X与目标成本与资源节点故障概率成正相关,不可用概率越高,X越高
对于一般应用容器集群,可考虑X = 0.2N

全链路压测(TODO)

  • 上述法则只能用于容量初估(大促压测前&新依赖),最终精准系统容量还是需要结合系统周期性压力测试得出。

应产出数据

  • 基于模型评估的入口流量模型 & 集群自身容量转化结果(若为非入口应用,则为限流点梳理)。
  • 基于链路梳理的分支流量模型 & 外部依赖容量转化结果。

大促保障

Incident Response - 紧急&前置预案梳理

要想在大促高并发流量场景下快速对线上紧急事故进行响应处理,仅仅依赖值班同学临场发挥是远远不够的。争分夺秒的情况下,无法给处理人员留有充足的策略思考空间,而错误的处理决策,往往会导致更为失控严重的业务&系统影响。因此,要想在大促现场快速而正确的响应问题,值班同学需要做的是选择题(Which),而不是陈述题(What)。而选项的构成,便是我们的业务&系统预案。从执行时机与解决问题属性来划分,预案可分为技术应急预案、技术前置预案、业务应急预案、业务前置预案等四大类。结合之前的链路梳理和业务评估结果,我们可以快速分析出链路中需要的预案,遵循以下原则:

  • 技术应急预案:该类预案用于处理系统链路中,某层次节点不可用的情况,例如技术/业务强依赖、弱稳定性、高风险等节点不可用等异常场景。
  • 技术前置预案:该类预案用于平衡整体系统风险与单节点服务可用性,通过熔断等策略保障全局服务可靠。例如弱稳定性&弱依赖服务提前降级、与峰值流量时间冲突的离线任务提前暂定等。
  • 业务应急预案:该类预案用于应对业务变更等非系统性异常带来的需应急处理问题,例如业务数据错误(数据正确性敏感节点)、务策略调整(配合业务应急策略)等
  • 业务前置预案:该类预案用于配和业务全局策略进行的前置服务调整(非系统性需求)

应产出数据

完成该项梳理工作后,我们应该产出以下数据:

  • 执行&关闭时间(前置预案)
  • 触发阈值(紧急预案,须关联相关告警)
  • 关联影响(系统&业务)
  • 决策&执行&验证人员
  • 开启验证方式
  • 关闭阈值(紧急预案)
  • 关闭验证方式

阶段性产出-全链路作战地图

进行完上述几项保障工作,我们基本可得到全局链路作战地图,包含链路分支流量模型、强弱依赖节点、资损评估、对应预案&处理策略等信息。大促期间可凭借该地图快速从全局视角查看应急事件相关影响,同时也可根据地图反向评估预案、容量等梳理是否完善合理。

Incident Response - 作战手册梳理

作战手册是整个大促保障的行动依据,贯穿于整个大促生命周期,可从事前、事中、事后三个阶段展开考虑。整体梳理应本着精准化、精细化的原则,理想状态下,即便是对业务、系统不熟悉的轮班同学,凭借手册也能快速响应处理线上问题。
事前
1)前置检查事项清单

  • 大促前必须执行事项checklist,通常包含以下事项:
  • 集群机器重启 or 手动FGC
  • 影子表数据清理
  • 检查上下游机器权限
  • 检查限流值
  • 检查机器开关一致性
  • 检查数据库配置
  • 检查中间件容量、配置(DB\缓存\NoSQL等)
  • 检查监控有效性(业务大盘、技术大盘、核心告警)
  • 每个事项都需包含具体执行人、检查方案、检查结果三列数据
    2)前置预案
  • 域内所有业务&技术前置预案。

事中

  1. 紧急技术&业务预案
    需要包含的内容基本同前置预案,差异点如下:
  • 执行条件&恢复条件:具体触发阈值,对应监控告警项。
  • 通知决策人。
  1. 应急工具&脚本
    常见故障排查方式、核心告警止血方式(强弱依赖不可用等),业务相关日志捞取脚本等。
  2. 告警&大盘
  • 应包含业务、系统集群及中间件告警监控梳理结果,核心业务以及系统大盘,对应日志数据源明细等数据:
  • 日志数据源明细:数据源名称、文件位置、样例、切分格式。
  • 业务、系统集群及中间件告警监控梳理结果:关联监控指标(链接)、告警关键级别、是否推送GOC、是否产生资损、是否关联故障、是否关联预案。
  • 核心业务&系统大盘:大盘地址、包含指标明细(含义、是否关联告警、对应日志)。
  1. 上下游机器分组
  • 应包含核心系统、上下游系统,在不同机房、单元集群分组、应用名,可用于事前-机器权限检查、事中-应急问题排查黑屏处理。
  1. 值班注意事项
  • 包含每班轮班同学值班必做事项、应急变更流程、核心大盘链接等。
  1. 核心播报指标
  • 包含核心系统&服务指标(CPU\LOAD\RT)、业务关注指标等,每项指标应明确具体监控地址、采集方式。
  1. 域内&关联域人员通讯录、值班
  • 包含域内技术、TL、业务方对应排班情况、联系方式(电话),相关上下游、基础组件(DB、中间件等)对应值班情况。
  1. 值班问题记录
  • 作战记录,记录工单、业务问题、预案(前置\紧急)(至少包含:时间、问题描述(截图)、影响分析、决策&解决过程等)。值班同学在值班结束前,进行记录。
    事后
  1. 系统恢复设置事项清单(限流、缩容)
    一般与事前检查事项清单对应,包含限流阈值调整、集群缩容等大促后恢复操作。
  2. 大促问题复盘记录
  • 应包含大促遇到的核心事件总结梳理。

沙盘推演和演练 Incident Response

实战沙盘演练是应急响应方面的最后一项保障工作,以历史真实故障CASE作为应急场景输入,模拟大促期间紧急状况,旨在考验值班同学们对应急问题处理的响应情况。
一般来说,一个线上问题从发现到解决,中间需要经历定位&排查&诊断&修复等过程,总体遵循以下几点原则:

  • 尽最大可能让系统先恢复服务,同时为根源调查保护现场(机器、日志、水位记录)。
  • 避免盲目搜索,依据白盒监控针对性诊断定位。
  • 有序分工,各司其职,避免一窝蜂失控乱象。
  • 依据现场情况实时评估影响范围,实在无法通过技术手段挽救的情况(例如强依赖不可用),转化为业务问题思考(影响范围、程度、是否有资损、如何协同业务方)。
  • 沙盘演练旨在检验值班同学故障处理能力,着重关注止血策略、分工安排、问题定位等三个方面:
    国际化中台双11买家域演练
    根据故障类型,常见止血策略有以下解决思路:
  • 入口限流:调低对应Provider服务来源限流值
  • 应对突发流量过高导致自身系统、下游强依赖负载被打满。
  • 下游降级:降级对应下游服务
  • 下游弱依赖不可用。
  • 下游业务强依赖经业务同意后降级(业务部分有损)。
  • 单点失败移除:摘除不可用节点
  • 单机水位飙高时,先下线不可用单机服务(无需下线机器,保留现场)。
  • 应对集群单点不可用、性能差。
  • 切换:单元切流或者切换备份

应对单库或某单元依赖因为自身原因(宿主机或网络),造成局部流量成功率下跌下跌。
Google SRE中,对于紧急事故管理有以下几点要素:

  • 嵌套式职责分离,即分确的职能分工安排
  • 控制中心\作战室
  • 实时事故状态文档
  • 明确公开的职责交接
  • 其中嵌套式职责分离,即分确的职能分工安排,达到各司其职,有序处理的效果,一般可分为下列几个角色:
    事故总控:负责协调分工以及未分配事务兜底工作,掌握全局概要信息,一般为PM/TL担任。
    事务处理团队:事故真正处理人员,可根据具体业务场景&系统特性分为多个小团队。团队内部存在域内负责人,与事故总控人员进行沟通。
    发言人:事故对外联络人员,负责对事故处理内部成员以及外部关注人员信息做周期性信息同步,同时需要实时维护更新事故文档。
    规划负责人:负责外部持续性支持工作,比如当大型故障出现,多轮排班轮转时,负责组织职责交接记录

资损体系

定期review资损风险

事中及时发现


【得物技术】浅谈资损防控

事后复盘和知识沉淀

参考学习

风控体系

性能优化


学习资料:

kafka 特点和使用场景

  • kafka具有高吞吐、低延迟、分布式容错、持久化、可扩展的特点,常用于系统之间的异步解偶,相比接口调用,减少单个服务的复杂性
  • 场景1: 系统间不同模块的异步解偶,例如电商系统的订单和发货
  • 场景2:系统或者用户日志的采集、异步分析、持久化
  • 场景3: 保存收集流数据,以提供之后对接的Storm或其他流式计算框架进行处理。例如风控系统
  • 异步事件系统

基本概念和组成

  • broker: Kafka 集群包含一个或多个服务器,服务器节点称为broker。broker 是消息的代理,Producers往Brokers里面的指定Topic中写消息,Consumers从Brokers里面拉取指定Topic的消息,然后进行业务处理,broker在中间起到一个代理保存消息的中转站。
  • producer和client id。生产者即数据的发布者,该角色将消息发布到Kafka的topic中。broker接收到生产者发送的消息后,broker将该消息追加到当前用于追加数据的segment文件中。生产者发送的消息,存储到一个partition中,生产者也可以指定数据存储的partition。
  • Consumer 、Consumer Group 和 group id。消费者可以从broker中读取数据。消费者可以消费多个topic中的数据。每个Consumer属于一个特定的Consumer Group。这是kafka用来实现一个topic消息的广播(发给所有的consumer)和单播(发给任意一个consumer)的手段。一个topic可以有多个CG。topic的消息会复制-给consumer。如果需要实现广播,只要每个consumer有一个独立的CG就可以了。要实现单播只要所有的consumer在同一个CG。用CG还可以将consumer进行自由的分组而不需要多次发送消息到不同的topic。
  • topic。类似于kafka中表名,每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处)
  • Partition 和 offset
    topic中的数据分割为一个或多个partition。每个topic至少有一个partition。每个partition中的数据使用多个segment文件存储。partition中的数据是有序的,不同partition间的数据丢失了数据的顺序。如果topic有多个partition,消费数据时就不能保证数据的顺序。在需要严格保证消息的消费顺序的场景下,需要将partition数目设为1。
  • Leader 和 follower。每个partition有多个副本,其中有且仅有一个作为Leader,Leader是当前负责数据的读写的partition。Follower跟随Leader,所有写请求都通过Leader路由,数据变更会广播给所有Follower,Follower与Leader保持数据同步。如果Leader失效,则从Follower中选举出一个新的Leader。当Follower与Leader挂掉、卡住或者同步太慢,leader会把这个follower从“in sync replicas”(ISR)列表中删除,重新创建一个Follower。
  • zookeeper。zookeeper 是一个分布式的协调组件,早期版本的kafka用zk做meta信息存储,consumer的消费状态,group的管理以及 offset的值。考虑到zk本身的一些因素以及整个架构较大概率存在单点问题,新版本中逐渐弱化了zookeeper的作用。新的consumer使用了kafka内部的group coordination协议,也减少了对zookeeper的依赖,但是broker依然依赖于ZK,zookeeper 在kafka中还用来选举controller 和 检测broker是否存活等等

可靠性语义、幂等性

生产者producer

业务上需要考关注失败、丢失、重复三个问题

  • 消费发送失败:消息写入失败是否需要ack,是否需要重试
  • 消息发送重复:同一条消息重复写入对系统产生的影响
  • 消息发送丢失:消息写入成功,但是由于kafka内部的副本、容错机制,导致消息丢失对系统产生的影响

三种语义

  • 至少一次语义(At least once semantics):如果生产者收到了Kafka broker的确认(acknowledgement,ack),并且生产者的acks配置项设置为all(或-1),这就意味着消息已经被精确一次写入Kafka topic了。然而,如果生产者接收ack超时或者收到了错误,它就会认为消息没有写入Kafka topic而尝试重新发送消息。如果broker恰好在消息已经成功写入Kafka topic后,发送ack前,出了故障,生产者的重试机制就会导致这条消息被写入Kafka两次,从而导致同样的消息会被消费者消费不止一次。每个人都喜欢一个兴高采烈的给予者,但是这种方式会导致重复的工作和错误的结果。
  • 至多一次语义(At most once semantics):如果生产者在ack超时或者返回错误的时候不重试发送消息,那么消息有可能最终并没有写入Kafka topic中,因此也就不会被消费者消费到。但是为了避免重复处理的可能性,我们接受有些消息可能被遗漏处理。
  • 精确一次语义(Exactly once semantics): 即使生产者重试发送消息,也只会让消息被发送给消费者一次。精确一次语义是最令人满意的保证,但也是最难理解的。因为它需要消息系统本身和生产消息的应用程序还有消费消息的应用程序一起合作。比如,在成功消费一条消息后,你又把消费的offset重置到之前的某个offset位置,那么你将收到从那个offset到最新的offset之间的所有消息。这解释了为什么消息系统和客户端程序必须合作来保证精确一次语义

实践
Kafka消息发送有两种方式:同步(sync)和异步(async),默认是同步方式,可通过producer.type属性进行配置。Kafka通过配置request.required.acks属性来确认消息的生产:

  • 0 —表示不进行消息接收是否成功的确认;
  • 1 —表示当Leader接收成功时确认;
  • -1—表示Leader和Follower都接收成功时确认

综上所述,有6种消息生产的情况,下面分情况来分析消息丢失的场景:

  • acks=0,不和Kafka集群进行消息接收确认,则当网络异常、缓冲区满了等情况时,消息可能丢失;
  • acks=1、同步模式下,只有Leader确认接收成功后但挂掉了,副本没有同步,数据可能丢失;

通常来说,producer 采用at least once方式

消息消费consumer

  • 重复消息的幂等性:由于生产者可能多次投递和消费者commit机制等原因,消费者重复消费是很常见的问题,需要思考系统对于幂等性的要求。在很多场景下, 比如写db、redis是天然的幂等性,某些特殊的场景,可以根据唯一id,借助例如redis判别是否消费过来实现消费者的幂等性
  • 消息丢失:评估消息丢失的影响和容忍度
  • commit:考虑auto commit 和 mannul commit

监控topic消息堆积情况(lag)

在实际业务场景中,由于consumer消费速度慢于producer的速度,会造成消息堆积,最终会导致消息过期删除丢失。业务需要监控这种lag情况,并及时告警出来。

另外需要注意的是,kafka只允许单个分区的数据被一个消费者线程消费,如果消费者越多意味着partition也要越多。

然而在分区数量有限的情况下,消费者数量也就会被限制。在这种约束下,如果消息堆积了该如何处理?

消费消息的时候直接返回,然后启动异步线程去处理消息,消息如果再处理的过程中失败的话,再重新发送到kafka中。

  • 增加分区数量
  • 优化消费速度
  • 增加并行度,找多个人消化

Rebalance 机制以及可能产生的影响

Rebalance本身是Kafka集群的一个保护设定,用于剔除掉无法消费或者过慢的消费者,然后由于我们的数据量较大,同时后续消费后的数据写入需要走网络IO,很有可能存在依赖的第三方服务存在慢的情况而导致我们超时。Rebalance对我们数据的影响主要有以下几点:

  • 数据重复消费: 消费过的数据由于提交offset任务也会失败,在partition被分配给其他消费者的时候,会造成重复消费,数据重复且增加集群压力
  • Rebalance扩散到整个ConsumerGroup的所有消费者,因为一个消费者的退出,导致整个Group进行了Rebalance,并在一个比较慢的时间内达到稳定状态,影响面较大
  • 频繁的Rebalance反而降低了消息的消费速度,大部分时间都在重复消费和Rebalance
  • 数据不能及时消费,会累积lag,在Kafka的超过一定时间后会丢弃数据
  • https://zhuanlan.zhihu.com/p/46963810

kafka是怎么做到高性能

Kafka虽然除了具有上述优点之外,还具有高性能、高吞吐、低延时的特点,其吞吐量动辄几十万、上百万。

  • 磁盘顺序写入。Kafka的message是不断追加到本地磁盘文件末尾的,而不是随机的写入。所以Kafka是不会删除数据的,它会把所有的数据都保留下来,每个消费者(Consumer)对每个Topic都有一个offset用来表示 读取到了第几条数据 。
  • 操作系统page cache,使得kafka的读写操作基本基于内存,提高读写的性能
  • 零拷贝,操作系统将数据从Page Cache 直接发送socket缓冲区,减少内核态和用户态的拷贝
  • 消息topic分区partition、segment存储,提高数据操作的并行度。
  • 批量读写和批量压缩
    Kafka速度的秘诀在于,它把所有的消息都变成一个批量的文件,并且进行合理的批量压缩,减少网络IO损耗,通过mmap提高I/O速度,写入数据的时候由于单个Partion是末尾添加所以速度最优;读取数据的时候配合sendfile直接暴力输出。
  • https://blog.csdn.net/kzadmxz/article/details/101576401

Kafka文件存储机制

  • 逻辑上以topic进行分类和分组
  • 物理上topic以partition分组,一个topic分成若干个partition,物理上每个partition为一个目录,名称规则为topic名称+partition序列号
  • 每个partition又分为多个segment(段),segment文件由两部分组成,.index文件和.log文件。通过将partition划分为多个segment,避免单个partition文件无限制扩张,方便旧的消息的清理。

kafka partition 副本ISR机制保障高可用性

  • 为了保障消息的可靠性,kafka中每个partition会设置大于1的副本数。
  • 每个patition都有唯一的leader
  • partition的所有副本称为AR。所有的副本(replicas)统称为Assigned Replicas,即AR。ISR是AR中的一个子集,由leader维护ISR列表,follower从leader同步数据有一些延迟(包括延迟时间replica.lag.time.max.ms和延迟条数replica.lag.max.messages两个维度, 当前最新的版本0.10.x中只支持replica.lag.time.max.ms这个维度),任意一个超过阈值都会把follower剔除出ISR, 存入OSR(Outof-Sync Replicas)列表,新加入的follower也会先存放在OSR中。AR=ISR+OSR
  • partition 副本同步机制。Kafka的复制机制既不是完全的同步复制,也不是单纯的异步复制。事实上,同步复制要求所有能工作的follower都复制完,这条消息才会被commit,这种复制方式极大的影响了吞吐率。而异步复制方式下,follower异步的从leader复制数据,数据只要被leader写入log就被认为已经commit,这种情况下如果follower都还没有复制完,落后于leader时,突然leader宕机,则会丢失数据。而Kafka的这种使用ISR的方式则很好的均衡了确保数据不丢失以及吞吐率
    当producer向leader发送数据时,可以通过request.required.acks参数来设置数据可靠性的级别:
    • 1(默认):这意味着producer在ISR中的leader已成功收到数据并得到确认。如果leader宕机了,则会丢失数据。
    • 0:这意味着producer无需等待来自broker的确认而继续发送下一批消息。这种情况下数据传输效率最高,但是数据可靠性确是最低的。
    • -1:producer需要等待ISR中的所有follower都确认接收到数据后才算一次发送完成,可靠性最高。但是这样也不能保证数据不丢失,比如当ISR中只有leader时(前面ISR那一节讲到,ISR中的成员由于某些情况会增加也会减少,最少就只剩一个leader),这样就变成了acks=1的情况。
  • ISR 副本选举leader
  • https://blog.csdn.net/u013256816/article/details/71091774

配置参数

  • kafka producer和consumer提供了大量打配置参数,很多问题可以通过参数来进行优化,常用了有下面参数
  • https://github.com/Shopify/sarama/blob/v1.37.2/config.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    c.Producer.MaxMessageBytes = 1000000
    c.Producer.RequiredAcks = WaitForLocal
    c.Producer.Timeout = 10 * time.Second
    c.Producer.Partitioner = NewHashPartitioner
    c.Producer.Retry.Max = 3
    c.Producer.Retry.Backoff = 100 * time.Millisecond
    c.Producer.Return.Errors = true
    c.Producer.CompressionLevel = CompressionLevelDefault

    c.Consumer.Fetch.Min = 1
    c.Consumer.Fetch.Default = 1024 * 1024
    c.Consumer.Retry.Backoff = 2 * time.Second
    c.Consumer.MaxWaitTime = 500 * time.Millisecond
    c.Consumer.MaxProcessingTime = 100 * time.Millisecond
    c.Consumer.Return.Errors = false
    c.Consumer.Offsets.AutoCommit.Enable = true
    c.Consumer.Offsets.AutoCommit.Interval = 1 * time.Second
    c.Consumer.Offsets.Initial = OffsetNewest
    c.Consumer.Offsets.Retry.Max = 3

kafka 常用命令

  • 创建topic

    1
    bin/kafka-topics.sh --create --topic topic-name --replication-factor 2 --partitions 3 --bootstrap-server ip:port
  • 查看topic情况

    1
    2
    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
    2
    3

    ./bin/kafka-consumer-groups.sh --group group_name --bootstrap-server brokers --reset-offsets --all-topics --to-latest --execute

推荐阅读

  1. kafka数据可靠性深度解读
  2. kafka 选举
  3. Kafka为什么吞吐量大、速度快?
  4. 简单理解 Kafka 的消息可靠性策略
  5. Bootstrap server vs zookeeper in kafka?
  6. kafka 如何保证顺序消费

基本使用

创建index,setting和mapping

1
curl -XPUT -H'Content-Type: application/json'  host/index_name?pretty=true -d@index_mapping.json 
es index example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
 {
"settings": {
"index": {
"number_of_shards": "5",
"number_of_replicas": "2"
},
"analysis": {
"filter": {
"t2sconvert": {
"convert_type": "t2s",
"type": "stconvert"
}
},
"analyzer": {
"traditional_chinese_analyzer": {
"filter": "t2sconvert",
"type": "custom",
"tokenizer": "ik_smart"
}
},
"normalizer": {
"lowercase": {
"type": "custom",
"filter": [
"lowercase"
]
}
}
}
},
"mappings": {
"_doc": {
"properties": {
"key_type": {
"type": "integer"
},
"country": {
"type": "keyword"
},
"language_code": {
"type": "keyword"
},
"level": {
"type": "integer"
},
"code": {
"type": "keyword"
},
"is_available": {
"type": "boolean"
},
"is_popular": {
"type": "boolean"
},
"pop_rank": {
"type": "integer"
},
"name": {
"properties": {
"value_in_chinese": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"value_in_english": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase"
}
}
}
}
},
"display_name": {
"properties": {
"value_in_chinese": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"value_in_english": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase"
}
}
}
}
},
"address": {
"properties": {
"value_in_chinese": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"value_in_english": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase"
}
}
}
}
},
"city_code": {
"type": "keyword"
},
"city_name": {
"properties": {
"value_in_chinese": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"value_in_english": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase"
}
}
}
},
"updated": {
"type": "date",
"format": "strict_date_optional_time||epoch_millis"
}
}
}
}
}
}

查看index,_cat 基本信息

1
curl -XGET 'host/_cat/indices/*hotel_basic_info_v2_live*(支持正则表达式)?v=true&pretty=true'

查看索引mapping信息

1
curl -XGET 'host/index_name/_mapping?pretty=true'

查看索引的setting信息

1
curl -XGET 'host/index_name/_settings?pretty=true'

通过doc id 正向查询

1
curl -XGET  'host/index/_doc/doc_id?pretty=true'

query,search,倒排查询

1
2
curl -XPOST -H'Content-Type: application/json' 'host/index_name/_search?pretty=true' -d '{
"query":{}}'

update

1
2
3
4
5
curl -XPOST  -H'Content-Type: application/json' 'host/index/_doc/doc_id/_update' -d '{
"doc": {
"price": "6500000001"
}
}'

聚合count查询

1
2
3
4
5
6
7
curl -XPOST -H'Content-Type: application/json' 'host/index_name/_count' -d '{
"query": {
"term": {
"city_name.value_in_english.keyword": "Jakarta"
}
}
}'

增加字段

1
2
3
4
5
6
7
8
curl -XPOST -H'Content-Type: application/json' 'host/index_name/_doc/_mapping' -d '{
"properties": {
"facility_codes": {
"type":"keyword"
}
}
}'

analyzer

在 Elastic Search 中,分词器起到了非常重要的作用,在定义文档结构、录入和更新文档、查询文档的时候都会用到它。例如:

1
2
3
4
5
6
7
8
9
10
武汉市长江大桥欢迎您

默认分词器:
[武, 汉, 市, 长, 江, 大, 桥, 欢, 迎, 您]

普通分词器:
[武汉, 市, 武汉市, 长江, 大桥,长江大桥, 欢迎, 您, 欢迎您]

二哈分词器:
[武汉, 市长, 江大桥, 欢迎, 您]

normalizer

alias

1
2
3
4
5
6
7
POST /_aliases
{
"actions": [
{"remove": {"index": "l1", "alias": "a1"}},
{"add": {"index": "l1", "alias": "a2"}}
]
}
1
curl -XPUT  host/index_nane/_alias/index_alias_name

query DSL

es query dsl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
{
"query": {
"function_score": {
"functions": [
{
"filter": {
"term": {
"key_type": 1
}
},
"weight": 3
},
{
"filter": {
"term": {
"key_type": 2
}
},
"weight": 2
}
],
"min_score": 0,
"query": {
"dis_max": {
"queries": [
{
"function_score": {
"functions": [
{
"filter": {
"term": {
"search_key.filed1.keyword": "querywords"
}
},
"weight": 100
},
{
"filter": {
"term": {
"search_key.filed2.keyword": "querywords"
}
},
"weight": 100
}
],
"score_mode": "max"
}
},
{
"dis_max": {
"queries": [
{
"match_phrase": {
"search_key.filed1": {
"boost": 50,
"query": "querywords"
}
}
},
{
"match_phrase": {
"search_key.field2": {
"boost": 50,
"query": "querywords"
}
}
}
]
}
},
{
"dis_max": {
"queries": [
{
"prefix": {
"search_key.filed1.keyword": {
"boost": 30,
"value": "querywords"
}
}
},
{
"prefix": {
"search_key.field2.keyword": {
"boost": 30,
"value": "querywords"
}
}
}
]
}
},
{
"dis_max": {
"queries": [
{
"match": {
"search_key.field1": {
"boost": 10,
"fuzziness": "auto:6,20",
"minimum_should_match": "3>75%",
"query": "querywords"
}
}
},
{
"match": {
"search_key.field2": {
"boost": 10,
"fuzziness": "auto:6,20",
"minimum_should_match": "3>75%",
"query": "querywords"
}
}
}
]
}
}
]
}
}
}
},
"size": 30,
"sort": [
{
"_score": {
"order": "desc"
}
},
{
"key_type": {
"order": "asc"
}
},
{
"others": {
"order": "desc"
}
}
]
}

原理

基本概念

  • 节点:分布系统都有的master节点和普通节点。类似于kafka集群都会存在的一种节点
  • master节点:用于管理索引(创建索引、删除索引)、分配分片,维护元数据
  • 协调节点:ES的特殊性,需要由一个节点汇总多个分片的query结果。节点是否担任协调节点可通过配置文件配置。例如某个节点只想做协调节点:node.master=false,node.data=false
  • ES的读写流程主要是协调节点,主分片节点、副分片节点间的相互协调。
  • ES的读取分为GET和Search两种操作。GET根据文档id从正排索引中获取内容;Search不指定id,根据关键字从倒排索引中获取内容。

写单个文档的流程

  1. 客户端向集群中的某个节点发送写请求,该节点就作为本次请求的协调节点
  2. 协调节点使用文档ID来确定文档属于某个分片,再通过集群状态中的内容路由表信息获知该分片的主分片位置,将请求转发到主分片所在节点;
  3. 主分片节点上的主分片执行写操作。如果写入成功,则它将请求并行转发到副分片所在的节点,等待副分片写入成功。所有副分片写入成功后,主分片节点向协调节点报告成功,协调节点向客户端报告成功。

读取单个文档的流程

  1. 客户端向集群中的某个节点发送读取请求,该节点就作为本次请求的协调节点;
  2. 协调节点使用文档ID来确定文档属于某个分片,再通过集群状态中的内容路由表信息获知该分片的副本信息,此时它可以把请求转发到有副分片的任意节点读取数据。
  3. 协调节点会将客户端请求轮询发送到集群的所有副本来实现负载均衡。
  4. 收到读请求的节点将文档返回给协调节点,协调节点将文档返回给客户端

Search流程

ES的Search操作分为两个阶段:query then fetch。需要两阶段完成搜索的原因是:在查询时不知道文档位于哪个分片,因此索引的所有分片都要参与搜索,然后协调节点将结果合并,在根据文档ID获取文档内容。

Query查询阶段

  1. 客户端向集群中的某个节点发送Search请求,该节点就作为本次请求的协调节点;
  2. 协调节点将查询请求转发到索引的每个主分片或者副分片中;
  3. 每个分片在本地执行查询,并使用本地的Term/Document Frequency信息进行打分,添加结果到大小为from+size的本地有序优先队列中;
  4. 每个分片返回各自优先队列中所有文档的ID和排序值给协调节点,协调节点合并这些值到自己的优先队列中,产生一个全局排序后的列表。

Fetch拉取阶段
query节点知道了要获取哪些信息,但是没有具体的数据,fetch阶段要去拉取具体的数据。相当于执行多次上面的GET流程

  1. 协调节点向相关的节点发送GET请求;
  2. 分片所在节点向协调节点返回数据;
  3. 协调阶段等待所有的文档被取得,然后返回给客户端。

es 更新和乐观锁控制

  • “_version” : 1,
  • “_seq_no” : 426,
  • “_primary_term” : 1,

性能优化

关注哪些性能指标

  • (读)query latency 1-2ms,复杂的查询可能到几十ms
  • (读)fetch latency ,QPS,读数据量,延时
  • (写)index rate,QPS,数据量,延时
  • (写)index latency
  • 存储数据量
  • 集群读写QPS,CPU、内存、存储、网络IO的监控
  • 节点维度的监控
  • index维度的监控

集群规划

  • 业务存储量,期望的SLA指标
  • 节点数量、内存、CPU数量,是否需要SSD等
  • 预留buffer,磁盘使用率达到85%、90%、95%
  • CPU使用率
  • 内存使用率
  • 冷热数据,灾备方案

settings 索引优化实践

  • 分片数量:number_of_shards,经验值:建议每个分片大小不要超过30GB。建议根据集群节点的个数规模,分片个数建议>=集群节点的个数。5节点的集群,5个分片就比较合理。注意:除非reindex操作,分片数是不可以修改的
  • 副本数量:number_of_replicas。除非你对系统的健壮性有异常高的要求,比如:银行系统。可以考虑2个副本以上。否则,1个副本足够。注意:副本数是可以通过配置随时修改的
  • refresh_interval 是一个参数,用于配置 Elasticsearch 中的索引刷新间隔。索引刷新是将内存中的数据写入磁盘以使其可搜索的过程。刷新操作会将新的文档和更新的文档写入磁盘,并使其在搜索结果中可见。默认值表示每秒执行一次刷新操作
  • 按照日期规划索引是个很好的习惯
  • 务必使用别名,ES不像mysql方面的更改索引名称。使用别名就是一个相对灵活的选择
  • setting中定义繁体全文检索时的traditional_chinese_analyzer以及一个名为lowercase的normalizer,常用于keyword类型的匹配
  • 结合profile、explain api 分析query慢的原因。search profile api
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{
"hotel_index_20220810": {
"settings": {
"index": {
"refresh_interval": "1s",
"number_of_shards": "5",
"provided_name": "hotel_index_20220810",
"creation_date": "1660127508475",
"analysis": {
"filter": {
"t2sconvert": {
"convert_type": "t2s",
"type": "stconvert"
}
},
"normalizer": {
"lowercase": {
"filter": [
"lowercase"
],
"type": "custom"
}
},
"analyzer": {
"traditional_chinese_analyzer": {
"filter": "t2sconvert",
"type": "custom",
"tokenizer": "ik_smart"
}
}
},
"number_of_replicas": "2",
"uuid": "afdjafkdlaf",
"version": {
"created": "6080599"
}
}
}
}
}

mapping 数据模型优化

  • 不要使用默认的mapping.默认Mapping的字段类型是系统自动识别的。其中:string类型默认分成:text和keyword两种类型。如果你的业务中不需要分词、检索,仅需要精确匹配,仅设置为keyword即可。根据业务需要选择合适的类型,有利于节省空间和提升精度,如:浮点型的选择.
  • Mapping各字段的选型流程
  • 选择合理的分词器。常见的开源中文分词器包括:ik分词器、ansj分词器、hanlp分词器、结巴分词器、海量分词器、“ElasticSearch最全分词器比较及使用方法” 搜索可查看对比效果。如果选择ik,建议使用ik_max_word。因为:粗粒度的分词结果基本包含细粒度ik_smart的结果。
  • 一个字段包含多种语言:分别设置了不同的分词器。中文:ik_max_word,英语:english等
  • analyzer:表示文档写入时的分词,search_analyzer表示检索时query的分词
  • type:text,type:keyword,不分词
  • normalizer 表示英文keyword判断时不区分大小写
  • “dynamic” : “strict”
  • https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
"properties": {
"accommodation": {
"properties": {
"value_in_chinese": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"value_in_english": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase"
}
},
"analyzer": "english"
},
"value_in_filipino": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "standard"
},
"value_in_indonesian": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "indonesian"
},
"value_in_malay": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "standard"
},
"value_in_thai": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "thai"
},
"value_in_tw_chinese": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "traditional_chinese_analyzer"
},
"value_in_vietnamese": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "standard"
}
}
}
}

数据写入优化

  1. 要不要秒级响应?Elasticsearch近实时的本质是:最快1s写入的数据可以被查询到。如果refresh_interval设置为1s,势必会产生大量的segment,检索性能会受到影响。所以,非实时的场景可以调大,设置为30s,甚至-1
  2. 能批量就不单条写入
  3. 减少副本,提升写入性能。写入前,副本数设置为0,写入后,副本数设置为原来值

读优化

  1. 分析dsl
  2. 禁用 wildcard模糊匹配,通过match_phrase和slop结合查询。
  3. 极小的概率使用match匹配
  4. 结合业务场景,大量使用filter过滤器
  5. 控制返回字段和结果,同理,ES中,_source 返回全部字段也是非必须的。要通过_source 控制字段的返回,只返回业务相关的字段。
  6. 分页深度查询和遍历.分页查询使用:from+size;遍历使用:scroll;并行遍历使用:scroll+slice

业务优化

  1. 字段抽取、倾向性分析、分类/聚类、相关性判定放在写入ES之前的ETL阶段进行
  2. 产品经理基于各种奇葩业务场景可能会提各种无理需求

SDK 使用

es migrate tools

拓展阅读

0%