你需要一个管家,随手召唤的那种,想吃啥就吃啥。

                       ——设计一个全局线程管理器

一个机器学习系统,需要管理一些公共的配置信息,如何存储这些配置信息,是一个难题。

设计模式

MVC框架

在传统的MVC编程框架中,通常采取设立数据中心的做法,将所有配置信息存在其中。

同时,将数据中心指针共享至所有类,形成一个以数据为中心,多重引用的设计模式。

如图,以MFC默认编程思路为例:

从零开始山寨Caffe·叁:全局线程管理器

这种编程框架,虽然思路清晰,但是需要将共享指针传来传去,显得相当赘余。

全局静态框架

这是一种新手程序员经常习惯干的事。

不设立封装型的数据中心,而是将配置信息写在全局静态变量中。

必要时,直接用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;
}
★int64_t Dragon::cluster_seedgen()

相关文章:

  • 2021-07-19
  • 2022-02-19
  • 2021-12-10
  • 2022-01-27
  • 2021-07-16
  • 2021-07-30
猜你喜欢
  • 2022-01-13
  • 2021-07-10
  • 2022-02-26
  • 2021-09-08
  • 2021-04-17
  • 2021-06-21
相关资源
相似解决方案