概要
源于facebook的faiss库,其用过GPU 对于进行加速,另外xfr中也用到GPU,而需求源于之前xid对于大数据量的性能问题。之前也了解过一些机器学习的平台,如tersorflow,paddlepaddle,他们均将GPU加速作为平台的一大“卖点”。因此思考GPU编程对于智能基础服务还是有很大的帮助,于是“贸然”涉足了这一领域。
CUDA编程给我的感觉是 1. 入门很容易,我大概用了一周的时间完成了对于cuda基础知识的学习,又用了一周的时间尝试将xid的核心函数迁移过来进行试验,并且尝试了多种优化方案,效果也都还不错;2. 进一步深入去运用发现很难,比如cuda同c++的混合编译,host和device的异步执行,包括对于细节的调优。在某一些理论上觉得会产生优化效果的地方,性能反而变差;对于一些理论,反复推究也不得其理。中途一直有放弃的念头,1是目前没有明确的应用,2是中途遇到的各种困难导致我也不确信是否能做出来。不过最终还是坚持写出来了一版。
整体工作分为两个部分。第一是尽量和xid的接口兼容,完成后能集成进xid进行测试验证。这部分工作完成后,一个较大的问题是GPU与CPU的传输数据很大,导致这部分的性能开销不能忽略;另一个是相对简单,很多CUDA的并行特性没有使用到,因此有了后面第二版的开发工作。它不在依赖xid的框架限制,接口设计更为灵活,在很多方面也对第一版的功能进行了补充。
| 特性 |
---|
第一版 |
- 支持多卡;
- 有内存池管理地址空间;
- 有对象池可以重复利用request相关的参数;
- 输出是所有item的distance,数据量较大;
|
第二版 |
- 支持多卡,并且可以控制使用的卡数;
- 有内存池管理地址空间,支持动态扩展;
- 有对象池可以重复利用request相关的参数,支持动态扩展;
- 支持数据的添加、更新和删除操作;
- 使用多stream支持并行请求,充分“压榨”GPU的性能;
- 输出是相似度最高的top k个数据,运算部分放到了gpu来做;
- 单元测试;
|
API
bool gpu_init(); // initialize void gpu_set_max_devices(const int used); // maximun gpu used void gpu_search(int block, // in which block const float* item, // 256 dim search item const int k, // top k return DistanceIndex* output); // top k distances return bool gpu_add_block(int block, // in which block const int num, // numbers to insert, batch mode const float* data); // insert data bool gpu_update_block(int block, // in which block const int seq, // the sequence number const float* data); // update data bool gpu_delete_block(int block, // in which block const int seq); // delete data bool gpu_clear_block(int block); // empty exact block data bool gpu_close(); // all data cleared bool gpu_dump_block(int block, // in which block int start, // for check propose, both start and end are inclusive int end, vector<vector<float> >& result); |
使用DEMO
struct DistanceIndex { float distance; // 欧式距离 int index; // 序列号 }; TEST(testCase,test1){ ASSERT_TRUE(xsearch::gpu_init()); // 初始化,准备资源 int data_num = 500000; // 测试50000万底库的数据 float* data = genBatchData(data_num); ASSERT_TRUE(xsearch::gpu_add_block(0, data_num, data)); // 添加数据 float* search_item = genBatchData(1); int top_k = 4; // 只返回距离最小的四个数据 DistanceIndex* ret = new DistanceIndex[4]; struct timeval start, end; // 记录时间开销 gettimeofday( &start, NULL ); gpu_search(0, search_item, top_k, ret); // 开始在50000万底库中查找距离最近的档案 gettimeofday( &end, NULL ); int cost = end.tv_sec*1000000+end.tv_usec - start.tv_sec*1000000-start.tv_usec; cout << "cost: " << cost << endl; showSearchResult(ret, top_k); ASSERT_TRUE(xsearch::gpu_close()); delete [] ret; delete [] search_item; delete [] data; } |
单卡跑50万底库的数据,时间开销8.3ms,符合之前的demo测试结果;
代码架构
核心类功能简介
类名 | 功能 |
---|
API | 对外不暴露的接口 |
DeviceManager |
- gpu卡的管理工作;
- 指定block数据存放在具体的卡;
|
DataManager | 对于档案数据的添加、更新和查找的管理工作; |
GpuDataImp |
- 对于档案数据的添加、更新执行工作;
- 作为查找接口的提供相应的“资源”;
|
KernelSearch | 计算当前档案和底库中所有档案的距离; |
TopK | 计算距离最近的top k个档案,返回档案的编号和距离值; |
GPU并行运算
当前的思路是,1. 先计算出当前特征值和底库中所有档案的距离;2. 选出top k个距离最小的值;关于1,前一篇wiki中已经介绍得很详细了,下面主要介绍2.
算法
- 在每个block中的thread先计算所管辖的数据,将top k个参数放入block的shared memory中;
- 使用reduction的方式,每个block整合shared memory中的数据,计算得出当前block top k的距离值和序列号;
- 重新启动一个核函数,只使用一个block number,类似于step 2的处理方式,计算得出最终的 top k的距离值和序列号;
优化方案
主要涉及到GPU的架构和CUDA算法
- GPU中每个SM是以warp的方式进行并行运算,同时只允许启动一个warp。每个warp包含了32个线程。shared memory是整个block中所有线程的共享空间,但是每个warp中的线程也可以获取到其它线程的变量值,并且理论上使用这种方法将提升性能;同时减少shared memory的使用量,可以提升SM管理block的数量(每个SM中shared memory的总量是一定的);
- 考虑到当前使用GPU是1080Ti,它允许每个block可以最大启动1024个thread,另外它包含了28个SM;
- 由于我们的算法涉及到两次核函数的调用,每次调用会涉及到一些参数值的选取。比如给定底库的总量total_num和选取topk的值,可以动态调整第一次核函数的thread_num和block_num,当total_num比较小或者topk较大时,可以让每个thread多处理一些数据;
- 临近数据的读取性能会更加,因此如何划分处理的数据也是有讲究的;