你需要一个管家,随手召唤的那种,想吃啥就吃啥。
——设计一个全局线程管理器
一个机器学习系统,需要管理一些公共的配置信息,如何存储这些配置信息,是一个难题。
设计模式
MVC框架
在传统的MVC编程框架中,通常采取设立数据中心的做法,将所有配置信息存在其中。
同时,将数据中心指针共享至所有类,形成一个以数据为中心,多重引用的设计模式。
如图,以MFC默认编程思路为例:
这种编程框架,虽然思路清晰,但是需要将共享指针传来传去,显得相当赘余。
全局静态框架
这是一种新手程序员经常习惯干的事。
不设立封装型的数据中心,而是将配置信息写在全局静态变量中。
必要时,直接用Get()函数获取。
Caffe恰恰使用了这种naive的做法,不过配上Boost之后,就相当powerful了。
多线程
一个线程除了需要些基础的配置信息,还需要什么?
在机器学习系统中,还需要随机数发生器。
随机数难题
产生一个随机数很简单,time(0)看起来不错,但是波动很有规律,是质量很低的随机数种子。
计算领域最常用的随机数发生器是 梅森旋转法 ,Caffe也使用了,这是一个兼有速度和质量的发生器。
ACM大神ACdreamers给出了一段模仿代码,ACM选手大概写了40行。
在CPU串行情况下,这40行代码怎么跑都行,但是在异步多线程中,问题就来了。
假设一个管理器中包含梅森旋转法实例对象,实例对象里有生成函数,且这个管理器只属于主进程,
当一个线程A需要随机数时,它可以访问主进程的梅森旋转法实例对象,执行该实例对象的产生函数。
梅森发生器每执行一次,其内部数据将有一次变动,我们可以将其视为对发生器的修改操作。
这个修改操作的触发对象大致有两个来源:
①DataTransformer,它在一个线程中调用。
②所有Layer参数的初始化,它在主进程中调用。
这样,如果梅森发生器只有一份,必然成为线程争夺的临界资源,它还包含修改操作。
临界资源不加mutex是危险的,如果我们为其加mutex,又有两处不便:
① 编程复杂,写mutex需要一番精力。
② 对临界资源产生阻塞访问,一定程度上降低了多线程的效率。
综合以上两点,随机数发生器最好设计成线程独立的资源。
设备难题
在多GPU情况下,我们需要为每一个GPU准备一个CPU线程来监督工作。
全局管理器会包含GPU设备信息(set device、get device)。
假设只有一个管理器,那么多个GPU该怎么去访问这个管理器,获得自己的设备编号?
显然,如果将管理器设计成线程独立的,那么这个问题就很好解决了。
同理,可以推广到root_solver这个属性。
显然,在主进程中,root_solver应该为true,默认它调度GPU0。
在监督其它GPU的CPU线程中,root_solver应该为false。
如果只有一个管理器,显然也是不妥的。
因而,随机数发生器必须具有线程独立性,进而,全局管理器必须有线程独立性。
Boost库提供了一个线程独立性智能指针,方便了线程独立资源的设计。
线程智能指针
boost::thread_specific_ptr<Class>是较为特殊的一个智能指针,但它不属于智能指针组,
位于"boost/thread/tss.hpp"下,属于boost::thread组。
通常将thread_specific_ptr指针设为全局static变量,进程和线程访问该指针时,将提供不同的结果。
其内部实现原理,应该是记录进程pid和线程tid,来做一个hash,以达到线程独立资源的管理。
static boost::thread_specific_ptr<Dragon> thread_instance; Dragon& Dragon::Get(){ if (!thread_instance.get()) thread_instance.reset(new Dragon()); return *(thread_instance.get()); }
将类静态函数Get封装之后,我们可以获得线程独立的管理器对象Dragon。
实例对象的代码空间将由Boost::thread控制,不在主进程的控制范围,
这样,Dragon管理器里的复杂代码,在执行时不会因为异步而被截断。
代码实战
随机数系统设计
建立rng.hpp,包含"boost/random/mersenne_twister.hpp"
typedef boost::mt19937 rng_t;
mt19937是梅森旋转法的一个32位实现版本,由boost提供,将至重命名为rng_t
建立common.hpp,创建管理器类Dragon。
———————————————————————————————————————————————————————————
首先,我们需要一个低质量的随机数种子,来初始化梅森旋转法。
同时,这个随机数种子,还必须是进程相关而不是线程相关的,避免多线程造成梅森随机数数值波动。
在Dragon管理器内部,声明静态成员函数:static int64_t cluster_seedgen();
建立common.cpp,实现这个低质量随机数种子发生器:
int64_t Dragon::cluster_seedgen(){ int64_t seed, pid, t; pid = _getpid(); t = time(0); seed = abs(((t * 181) *((pid - 83) * 359)) % 104729); //set it as you want casually return seed; }