深度学习框架-端侧训练性能问题分析

116 阅读3分钟

问题背景:用户使用java native方式调深度学习框架端侧训练C++ API,在x86服务器环境性能劣化严重,直接调框架C++ API跑端侧训练,性能正常。

初步分析:逐层打点,最后缩小范围到Matmul算子,并且SSE指令加速。根据java API -> c++ API-> MS框架调用流程,梳理可疑点: 1 jvm启动配置问题,jvm内存过小;jdk版本问题; 2 java native机制问题; 3 算子问题。

1 jvm配置和jdk版本

调大jvm配置参数,没有效果

-Xms1000M -Xmx1000M -Xmn500M
-XX:ThreadStackSize=1000M

升级最新Jdk17,同样没效果。

2 java native机制问题

JAVA调用SSE指令会变慢

在网上搜索案例java调用SSE变慢,这个案例发现java调用SSE变慢的原因:由于SSE指令“批处理”所有操作的方法。为了使批处理工作,每个方法调用的操作数以及每个操作的操作码必须存储在本机内存中,以便本机函数解码/读取和执行。操作码和操作数的存储和读取是主要瓶颈。

写两个demo验证:demo1 c++ SSE实现矩阵乘,demo2 java native方式调用demo1,统计执行性能分别是615.8ms、688.4ms。测试结果无法充分证明java jni会导致明显性能劣化。

CriticalNative可以提升性能

虽然无法正向证明java jni会产生明显性能劣化,但抱着试试心态,尝试优化jni性能开销,参考CriticalNative:降低JNI开销

调用native方法时,JVM的工作步骤:

创建栈帧;
根据ABI移动参数到寄存器或者栈;(ABI: 应用二进制接口)
封装对象引用到JNI handlers;
获取静态方法的JNIEnv*和jclass,把他们作为额外参数传递;
检查是否调用method_entry的trace函数;
检查是否调用对象锁;(synchronized)(optinal)
检查native方法是否已经链接;(懒加载函数检查、链接)
线程状态从in_java转变为in_native;
调用native方法;
检查是否需要safepoint;
线程状态转回in_java;
解锁对象锁;(optional)
notify method_exit;(optional)
将对象结果解出,重置JNI handlers;
处理JNI异常;
移除栈帧。

开销用于各种参数拷贝,尤其是遇到数组,需要来回拷贝、检查。 Critical Natives来降低开销。

封装Critical Natives,验证测试step time仍然很高,性能还是30ms(c++接口性能只有7ms)

3 算子问题

排查数据问题

1 排查Matmul算子输入data是否异常(排查无NAN、INF值);
2 打印Matmul SSE汇编指令耗时(逐条打印指令耗时没有差异,最小for循环耗时有差异;排除法分析, _mm_load_ps1会导致性能差异);但还是无法定位到根因,无法解决问题;

排查CPU性能

在jvm性能专家的帮助下,使用perf工具抓取日志,分析cache miss;
使用Inter CPU性能vtune工具分析后,发现CPU前端性能劣化严重,CPU前端主要做取指、指令译码,CPU前端性能差,怀疑Subnormal data运算。规避优化Subnormal data运算,性能立即提升到目标水平。

CPU 前端  // 取指令,译码
  复杂的指令解析情况比较少: Subnormal data运算
CPU 后端  // 访存

总结根因是:Matmul矩阵中出现subnormal floats,subnormal在Intel处理器上运算非常慢,解决手段是 设置相关寄存器 DAZ、FTZ,把subnormal floats归零,验证train step time已从30ms提升到5ms左右。

Linux性能分析工具Perf

Intel 次常规问题

vtune性能分析工具