最近在部署AI相关的算法,并要求减少总耗时,从中总结出的一些比较通用的优化技巧。精髓总结一句话就是:在同一时间尽可能充分利用硬件资源。而怎么尽可能充分利用呢,方式就是多线程并行处理。
1、单线程串行处理数据
假设算法需要处理两份同类型的数据(适用场景可以扩展到:算法需要处理两份及以上同类型的数据,或者对算法内部进行优化),最简单的就是采用单线程先后处理,如下图1:
图1 串行处理多份同类型数据
2、多线程并行处理同类型数据
串行方式的缺点是对硬件的使用率非常低,比如处理每份数据都要经过数据加载、计算、保存等操作,那么同一时间数据在加载传输到内存的时候cpu是空闲的,最好的情况是在加载传输第二份数据的时候cpu正在对第一份数据计算。提高各个硬件资源的利用率的方法就是“多线程并行”,如下图2所示创建两个线程,每个线程分别处理一份同类型数据:
图2 多线程并行处理同类型数据
c++实现代码如下:
#include <thread>
using namespace std;
auto func_proc=[](Ai* mai,float* data){
mai->Proc(data);
};
Ai mai1,mai2;
float* data1,data2;
thread t1(func_proc,&mai1,data1);
thread t2(func_proc,&mai2,data2);
t1.join();
t2.join();
3、拆分成各子模块之间的并行
但是上面这种并行方式资源占用是比较多的,相当于同时申请了两份Ai资源,如果Ai资源内存或者显存占用很多势必会影响其他算法性能。因此有一种解决方案就是对Ai进行拆分(如下图3所示),比如这里拆分成4个子模块,子模块之间尽可能解耦(可以有相互依赖关系)。这样当线程2处理子模块A时会先等待线程1执行完子模块A,当线程1执行完子模块B时,线程2执行子模块A,达到子模块之间并行。从理论上来说,对算法模块拆分越精细,则对硬件利用率越高。
图 3.各个子模块之间并行
代码上实现这种并行方式,可以对每个子模块加一个互斥锁,如下图4所示。
图 4.通过加互斥锁达到各子模块间的并行
c++代码如下:
#include <thread>
#include <mutex>
using namespace std;
auto func_proc=[&](Ai* mai,float* data,mutex* mylocks){
{
std::lock_guard<std::mutex> tmpLock(mylocks[0]);
auto outA=mai->subA(data);
}
{
std::lock_guard<std::mutex> tmpLock(mylocks[1]);
auto outB=mai->subB(outA);
}
{
std::lock_guard<std::mutex> tmpLock(mylocks[2]);
auto outC=mai->subC(outB);
}
{
std::lock_guard<std::mutex> tmpLock(mylocks[3]);
auto outD=mai->subD(outC);
}
};
Ai mai1;
float* data1,data2;
mutex mylocks[4];
thread t1(func_proc,&mai1,data1,mylocks);
thread t2(func_proc,&mai1,data2,mylocks);
t1.join();
t2.join();
4、独立子模块之间的并行
进一步,如果子模块之间有相互独立的,则可以并行起来,如下图所示:
图 5.独立子模块之间的并行