经验分享|昇思MindSpore仰宗焱:任何时候的努力都为时不晚。
经验分享|昇思MindSpore仰宗焱:任何时候的努力都为时不晚。
本期项目经验分享来自仰宗焱同学,承担的项目是【基于昇思MindSpore Quantum,对已有的梯度计算模块实现加速。】

01、关于昇思MindSpore项目介绍
1.项目名称
基于昇思MindSpore Quantum,对已有的梯度计算模块实现加速。
2.项目链接
https://summer-ospp.ac.cn/#/org/prodetail/221cb0173
3.项目导师
Tree
4.项目描述
量子机器学习的常用方法是通过计算目标哈密顿量期望对含参数量子线路中各参数的导数信息从而更新量子线路参数,使得模型获得更好的精确度/准确度。
量子机器学习中,一般训练集中包含许多样本数据。如何利用多核处理器并行的计算不同样本哈密顿量期望对参数的导数是个重要问题。本任务要求基于MindSpore Quantum现有的梯度计算功能,提升梯度计算的性能1倍以上。 02、项目开发理论描述
项目概述
简而言之,昇思MindSpore Quantum运行的基本原理就是使用矩阵模拟量子比特,从而在普通计算机上实现量子计算。其中势必存在大量的梯度求导运算,运行的时间长短直接影响着MindSpore Quantum运行的效率。任务的目标就是对原有的梯度计算模块加速,实现一倍效率的提升。
昇思MindSpore Quantum虽是Python包,但是对梯度求导这种速度敏感的模块,底层代码语言往往是运行效率更高的C++。在本任务中,MindSpore Quantum梯度计算调用模块为grad_ops函数,其底层为映射的C++计算函数。

调用逻辑
对函数进行加速的模块方法的选择其实是非常自由的,例如,底层的C++函数HermititianMeasureWithGrad在运行的过程中,会去调用其他的函数去辅助计算,可以将这些重复调用的函数放在HermititianMeasureWithGrad函数里面,这样函数调用的次数就变少了,函数多次调用所需要的开销就省去了。
也可以考虑从求梯度计算的算法结构上入手,用更高效的算法替代当前的算法。亦或是在多线程处理上下手,增加内存池或者线程池去优化代码在多线程中的处理逻辑,从而实现代码优化的目的。
以上三个思路中,毫无疑问从算法结构上去处理梯度计算的优化是最优雅的。但是开源项目的推进通常有着大量的数学和计算专家对算法进行共享,其算法的优化空间已经非常有限,所需要的时间、精力、知识面都是非常巨大的。倘若能在算法层面将当前主流的梯度计算方法的复杂度降低一个数量级,那我应该去考虑去参选下一届的图灵奖。
将函数写入HermititianMeasureWithGrad函数从而减少调用次数的这个思路也不大行得通,这固然在理论上节省了函数调用的开销,但是实际的效果却比较有限。因为当前的编译器也是非常智能的,检测到重复的调用,编译器往往就给你直接优化掉了。考虑到写死代码对程序耦合度的提升,即便这样的方法在一些细枝末节有所提升,但若优化并不是非常明显,就显得得不偿失了。
在多线程上下手是一个比较合理的优化方案,当前MindSpore Quantum的中目前使用的梯度计算模块,是通过多线程的方式实现对梯度计算的加速。然而,计算模块在每次进行函数调用的时候创建线程,等函数调用结束之后线程就被销毁,大量的线程资源申请和删除造成了比较高的性能浪费(线程的性能开销要远大于函数的性能开销)。
std::vector tasks;
tasks.reserve(batch_threads);
size_t end = 0;
size_t offset = n_prs / batch_threads;
size_t left = n_prs % batch_threads;
for (size_t i = θ; i < batch_threads; ++i) {
size_t start = end;
end = start + offset;
if(i < left){
end += 1;
}
auto task = [&, start, end](){
for (size_t n = start; n < end; n++) {
ParameterResolver pr = ParameterRe
pr.SetItems(enc_name, enc_data[n]);
pr.SetItems(ans_name, ans_data);
auto f_g = HermitianMeasureWithGrad(h
output[n] = f_g;
}
};
tasks.emplace_back(task);
}
for (auto &t : tasks){
t.join();
}
HermitianMeasureWithGrad 核心代码
因此,可以考虑使用线程池对计算模块进行加速,线程池提前创建若干线程,线程永久存在,若任务队列中有任务就执行,否则线程就阻塞,省去了创建和销毁线程的大量开销。
一个简略的线程池设计如下图,线程池结构体中,函数的队列用于存储待执行的函数。线程的Vector容器中的线程循环查询是否有待执行的函数队列,若有则从队列中取出执行。线程池的构造函数用于线程池的初始化,如线程容器的创建、函数队列的创建、初始变量的设定等。线程池的析构函数用于等待线程池线程的结束并关闭线程池。
Enqueue函数用于接受任务并加入等待的任务队列。GetInstance函数用于向其他函数提供访问线程池的渠道,其他函数通过GetInstance函数得到指向线程池的指针。考虑到线程池的创建和访问涉及到多线程操作,因此线程池将采用饿汉模式创建,在模拟器创建的时候即创建,从而杜绝创建同步冲突的问题。
当然,在本项目中还需要考虑到其他的问题,例如每个用户的电脑配置的是不一样的,因此我设计的线程池还提供了对线程池线程的修改、线程池线程的数量查询等功能。

一个基本的线程池结构体
线程池设计问题
**问题1:**线程池的原理是线程无限循环查询任务队列中是否有等待执行的任务,如果有则取出执行,执行结束后回到无限查询状态。因此,对于程序来说,只能判断当前存在N个线程,具体其中的线程是不是在执行任务并不知道,这让接下来的代码出现了很多问题。因此我在线程池中增加一个原子变量idle,记录目前空进程的数量,当队列中的任务被线程接受执行和任务结束后,修改idle的数量,从而实现当前是否有空进程的判断。

**问题2:**如果按照传统的线程池思路,多线程任务全部丢入Vector容器中,然后挨个对容器内的线程进行join操作,线程执行结束后,资源自动释放。但是在我们的优化的代码中并不能这样操作,因为线程池的生命周期就是MindSpore Quantum的生命周期,至少也得是量子模拟器的生命周期,线程需要在线程池中长期保持,如果使用join操作,则执行一个任务后线程释放,接下来对任务就不能执行了。因此,我使用到了问题1中创建的idle原子变量,在所有的任务丢入线程池的任务队列后,循环查询idle是否为0和线程池的任务队列是否为空。若同时满足,才会接触循环执行下一步的代码。
结果测试
测试操作系统为:Ubuntu 20.04
测试配置为:8核心处理器 8GB RAM 20GB SSD
测试方法为:引入chrono头文件,在C++函数HermitianMeasureWithGra中添加锚点计算函数的执行时间,比较改进前与改进后的时间,判断性能是否有提升,代码示例如下。
auto start = std::chrono::system_clock::now();
//Do Funciton
auto endt = std::chrono::system_clock::now();
std::chrono::duration elapsed = endt-start;
std::cout<<"Elapsed time: "<
为了防止实验误差对结果准确性造成的影响,每次测试执行多组实验,多次测试。对比每次、每组测试之间的耗时差异。

由图可以看出,实验组每组第一次的耗时约为0.8毫秒,对照组(即未优化版本)每组第一次耗时约为1毫秒,其提升约为20%。而每组其后开始,实验组的提升就非常可观了,实验组每组2—5次的耗时约为0.3—0.4毫秒,对照组每组2—5次耗时约为0.6—0.8毫秒,其速度提升达到了约一倍,达到了实验的要求。
实验出现这样的结果并不难理解,实验组在第一次调用函数的时候,需要初始化线程池,对照组同样需要创建线程(但是不需要销毁线程),因此仅有约20%的性能优化。而再次调用函数,此时实验组可以直接将任务送入线程池而不需要重新创建新的线程和销毁线程,从而实验了一倍的性能提升。
**03、随访**
**1.参与开源之夏**
**ospp:**请简单介绍一下你的开源经历吧。
**仰宗焱:**大家好,我叫仰宗焱,是上海海洋大学计算机科学与技术专业大四的一名本科生。目前在上海喜马拉雅科技有限公司的AI岗位实习。我对算法和AI有一定的了解。我觉得作为一个计算机专业的学生,算法是内功,打好内功可以让自己在未来走得更远。至于AI,它不一定是未来,却是一个非常酷炫的发展方向。在校期间,我参加过一些算法的竞赛,不过鉴于能力有限,只取得了一些差强人意的名次。我对AI领域的学习更多是兴趣使然,目前也发表了一篇AI相关的SCI论文。
说到开源,我很早,可能初中的时候,我就了解开源的精神了,知道去GitHub之类的网站下载开源的软件,也非常认同开源人人为我、我为人人的理念。但是很惭愧,在本次开源项目之前我只是开源的一个使用者,而非贡献者。一方面是我认为能够给开源项目做贡献的,都是一些技术大牛,就算不是顶尖大学的计算机教授,也是华为这样大公司的资深程序员。我这一个一文不名的学生何德何能去“僭越”呢?另一方面而言,对于一个初学者来说,开源社区的贡献流程是有一定的学习成本的,毕竟大学里从来没有一门课是教你如何给开源社区做贡献的(也可能只是我的大学没有)。如果没有一个“内行”指路,初学者看着一条条步骤,很容易望而却步。但是在我完成了开源之夏项目后,我发现我所认为的“门槛”并没那么多高,即便是我这样一个很普通的只是对相关领域感兴趣的学生,在社区的帮助下,也可以很好地对开源项目进行贡献。这也是我非常难忘的一次项目经历。
**ospp:**请问你是怎样了解到开源之夏的?
**仰宗焱:**我与开源之夏结缘要追溯到大二的暑假,当时辅导员在班级里面分享了开源之夏的活动。我的室友,一名高中信息竞赛生,也是一名开源社区的贡献者,觉得这是一次非常好的机会,毫不犹豫地参加了,同时也劝我一起参加。但是说出来有些丢人且戏剧性的是,在我弄完期末周的杂事准备报名的时候,我发现项目的截止日期是昨天。就这样,我稀里糊涂地错过了一次开源之夏的机会。这事也让我时常耿耿于怀,因此我下决心绝不能再错过大三暑假的开源之夏。
**ospp:**你的项目竞赛经历很丰富,请问这对于你此次所参与的项目有什么帮助吗?
**仰宗焱:**我确实参加过很多竞赛,主要是算法竞赛。我没有那些XCPC金牌选手的天赋,也不像那些高中信息竞赛选手,勤奋且很早就开始接触算法。我的能力其实很普通,大一才开始接触算法,为了兼顾学校的其他事情也不可能所有的时间都投入其中。因此虽然参加过很多竞赛,但大多是陪跑,偶尔拿一些差强人意的奖项。不过虽然能力有限,竞赛经历确实对于本次所参与的项目帮助颇多。首先,竞赛帮我打了一个相对不错的C语言基础,这让我在项目开发的时候对编程语言的掌控可以得心应手。其次,竞赛经历让我对程序的运行的效率非常敏感(毕竟在竞赛中往往几毫秒的差距就是天壤之别),这让我对项目开发的时候有一个明确优化方向,不会像无头苍蝇一样抓瞎。
**2.参与开源社区**
**ospp:**介绍一下你眼中的昇思MindSpore社区吧。
**仰宗焱:**由于各种各样的原因,国内的开源氛围和国外相比还有很大的发展空间。昇思MindSpore 社区通过开展各种各样的活动去宣传推广昇思MindSpore项目的发展。在我看来,维护昇思MindSpore,为广大AI计算者带来便利可能都是其次,毕竟类似的如Pytorch、TensorFlow这样的AI框架也同样优秀。昇思MindSpore社区的可贵之处在于,它极大地促进了国内开源建设的氛围,它举办的推广活动、社区的帮助,吸引了大量优秀的开发者参与其中,而以此为契机这些优秀的开发者很可能继续为国内的开源添砖加瓦,这对国内开源的积极影响可能要远大于其本身维护的 AI 框架。
**ospp:**在项目进行过程中有没有印象深刻的经历?社区和导师为你提供了怎样的帮助?
**仰宗焱:**在项目开发的过程中,我最深刻的经历是MindSpore Quantum框架的编译安装,是的环境的安装花费了我大量的时间。我的任务要求是对项目的底层代码进行优化,因此需要在源代码中进行修改再编译为安装包安装测试,而不能直接pip命令安装。而在项目的编译过程中,我遇到了很大的阻力。可能难以想象,我换了三台电脑都没能在Windows系统中成功编译,因为0.6版本的MindPore Quantum的Windows编译设置存在一些问题(当然在现在的版本中已经修复了),从而出现和系统不兼容的编译问题。而在Linux操作系统的编译中,也偶尔会出现一些不稳定的问题,最终通过大量时间进行测试,我选择在Linux虚拟机中进行开发,并选定了对应的程序版本。在这其中,导师给了我大量的建议和帮助。
**ospp:**你认为作为一名计算机专业的大学生参与到开源社区,对自己的成长和能力有什么积极作用?你是如何迈出第一步的?
**仰宗焱:**我觉得本次开源经历是我难忘的一次经历,我完整地经历了一次项目的开发与代码合并的流程。相比竞赛代码而言,编写工程代码需要考虑代码的耦合性、易读性、拓展性等因素,这极大锻炼了我的代码能力。同时,项目开发的过程中和社区的交流也锻炼了我的社交能力。项目开发的过程中,包括联系导师的过程中,我一直在告诉自己不要产生畏惧之情,需要困难就慢慢地去解决,调整好心态,一步步走下去,你会发现很快终点就在面前。
**3.收获与寄语**
**ospp:**开源之夏旨在培养和发掘更多优秀的开发者,通过这次的活动你有什么意想不到的收获和成长吗?
**仰宗焱:**相比较于技术上的成长,我觉得我在学习上有了更多的自信。在此之前,我一直认为为开源社区开发是遥不可及的事情,但是在本次活动后,我很好地完成了项目。这让我意识到,能力平庸影响并不大,只要从当下开始努力,任何时候学习都是为时不晚的。
**ospp:**通过这次参与开源项目的经历,你在自己涉猎的领域是否有所突破,能否分享一些你的学习经验。
**仰宗焱:**我对学习方法有了一些新的理解。我觉得面对一个看似寸步难行的大困难,不要止步不前,把问题一步步拆解成一个个小问题再去解决,只要你一步步在前进,离成功的相对距离就会越来越近。
**ospp:**对于爱好开源的学弟学妹你有什么想对他们说的吗?
**仰宗焱:**有句话叫,雄关漫道真如铁,而今迈步从头越。在面对一个全新的领域的时候,能力为零的我们难免会迷茫,但无论什么时候,请相信自己,不要害怕自己能力不足。东隅已逝,桑榆非晚。任何时候的努力都为时不晚。
**04、感悟**
我其实在大二暑假的时候就受朋友的介绍了解到了这个开源暑期的项目,感觉是一次很好的锻炼自己的机会,当然有奖金拿也是很棒的。但是很丢人的是,神经大条的我在想起来报名活动的时候,我发现截止日期是昨天,由此错失了去年参加的机会。因此,我告诉自己一定要抓住今年的这次机会,结果也如愿以偿,成功入选并结项,对我而言是一次很重要也很有意义的经历。
我投入了很多精力在项目上,但是就核心的代码层面来看,其实并不是很复杂。很多时间其实花费在了侧边的工作上。例如,MindSpore Quantum的项目迭代速度是非常之快的,我代码写好后并提交pr之后,MindSpore Quantum出现了版本的更新,我在旧版本基础上的修改同新版本出现了版本的冲突,造成了无法合并的问题,我只得又迁移到新版本上才使得项目顺利进行。
同时,整个项目是一次完整的开源活动,从量子计算的原理(其实本项目并没有涉及到量子计算,而是在C++代码层面进行了优化)、开源项目的工作流程到最基本的Git的使用,对那些优秀的开发者同学来说必然是轻车熟路甚至是不屑于提及的基本知识,但是对我这样的菜鸟来说,我需要一个个慢慢的去学习,一些看起来很愚蠢的问题,我都需要搜索或者提问,学习起来也遇到了很多的拦路虎。
不过,在我一个个解决了这些问题,一整个流程走通,看到了我的代码被社区合并时,我意识到未来会有成千上万的人会使用我的代码,我的贡献超越了政治、文化、国界,上升到乃至全人类的财富。这给我带来的成就感是巨大的,项目的奖金比起来,似乎也不值一提了。正因为这一份成就感,我在未来也会继续支持开源项目的发展。
在项目进行的过程中,我的体验是很好的,我受到了项目的导师Tree、开源之夏的同学以及MindSpore Quantum社区的很多帮助。前文所提到的一些问题,他们为我提供了很多的解决思路。在此我要向他们表达由衷的感谢。