从零开始山寨Caffe·叁:全局线程管理器
你需要一个管家,随手召唤的那种,想吃啥就吃啥。
——设计一个全局线程管理器
一个机器学习系统,需要管理一些公共的配置信息,如何存储这些配置信息,是一个难题。
设计模式
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();
seed = abs(((t * ) *((pid - ) * )) % ); //set it as you want casually
return seed;
}
★int64_t Dragon::cluster_seedgen()
Caffe利用pid、和默认时间、以及几个奇怪的数计算了一个低质量的随机数种子,
最外层的104729保证了种子的范围,你可以随便改成任何喜欢的数。
———————————————————————————————————————————————————————————
在Dragon内部声明且定义RNG类:
class RNG{
public:
RNG() { generator.reset(new Generator()); }
RNG(unsigned int seed) {generator.reset(new Generator(seed));}
rng_t* get_rng() { return generator->get_rng(); }
class Generator{
public:
//using pid generators a simple seed to construct RNG
Generator() :rng(new rng_t((uint32_t)Dragon::cluster_seedgen())) {}
//assign a specific seed to construct RNG
Generator(unsigned int seed) :rng(new rng_t(seed)) {}
rng_t* get_rng() { return rng.get(); }
private:
boost::shared_ptr<rng_t> rng;
};
private:
boost::shared_ptr<Generator> generator;
};
★class RNG
RNG类内嵌一个Generator发生器类,Generator内部又封装一个rng_t。
梅森发生器支持两种构造模式,指定种子或使用进程相关的低质量种子。
梅森发生器以动态内存的形式管理rng_t,rng_t* get_rng()函数需要特别注意。
Boost的rng_t,也就是boost::mt19937,本质是一个类,这个类内部设置了复制构造函数。
复制构造函数的内容很有趣——重置梅森算法状态,这意味着,如下代码会是个灾难:
rng_t a;
rng_t b=a;
如果梅森发生器a已经产生了一定随机数,那么将a赋值给b,b将重复a的前几个值,因为发生器被重置了。
如果需要获得发生器a的副本,应当使用指针来获取,这是为什么get_rng()要返回指针的原因。
在Dragon里定义一个静态成员函数的get_rng(),方便外部直接调用:
static rng_t* get_rng(){
if (!Get().random_generator){
Get().random_generator.reset(new RNG());
}
rng_t* rng = Get().random_generator.get()->get_rng();
return rng;
}
★static rng_t* get_rng()
它利用了线程独立管理器来构建管理器内部的RNG对象,并返回rng_t指针。
获得一个随机数很简单:
static unsigned int get_random_value(){
rng_t* rng = get_rng();
return (*rng)();
}
boost::mt19937同时重载了()函数,调用它直接可产生随机数,切记以指针解引用形式调用。
数据结构
class Dragon{
public:
Dragon();
~Dragon();
static Dragon& Get();
enum Mode{ CPU, GPU };
static Mode get_mode() { return Get().mode; }
static void set_mode(Mode mode) {Get().mode = mode;}
static int get_solver_count() { return Get().solver_count; }
static void set_solver_count(int val) { Get().solver_count = val; }
static bool get_root_solver() {return Get().root_solver;}
static void set_root_solver(bool val) {Get().root_solver = val;}
static void set_random_seed(unsigned int seed);
static void set_device(const int device_id);
static rng_t* get_rng();
static unsigned int get_random_value();
static int64_t cluster_seedgen();
class RNG{....}
#ifndef CPU_ONLY
static cublasHandle_t get_cublas_handle() { return Get().cublas_handle; }
static curandGenerator_t get_curand_generator() {return Get().curand_generator;}
#endif
private:
Mode mode;
int solver_count;
bool root_solver;
boost::shared_ptr<RNG> random_generator;
#ifndef CPU_ONLY
cublasHandle_t cublas_handle;
curandGenerator_t curand_generator;
#endif
};
★class Dragon
成员变量包括:
★工作模式:mode
★solver相关:solver_count和root_solver
★RNG:random_generator
★CUDA相关:cublas句柄cublas_handle、curand发生器curand_generator
成员函数包括:
★get系封装
★set系封装
★随机数系统相关
———————————————————————————————————————————————————————————
比较难以理解的是solver_count和root_solver,这涉及到分布式计算上。
新版Caffe允许多GPU间并行,与AlexNet不同,多GPU模式的内涵在于:“不共享数据,却共享网络”
所以,允许多个solver存在,且应用到不同的GPU上去。
直接使用solver_count的地方是DataReader,每一个DataLayer都有一个DataReader,
DataReader工作在异步线程,我们允许在一个主程序上跑多个DataLayer,但是不可以有多个ConvLayer。
关于多GPU的介绍请看第肆章。
第一个solver会成为root_solver,第二、第三个solver就会成为shared_solver。
root_solver有很大一部分特权,具体有以下几点:
★LOG(INFO)允许信息:显然我们不需要让几个GPU,产生几份重复的信息。
★测试:只有root_solver才能测试,猜测是为了减少冗余计算?
★统计结果:只有root_solver才能输出统计结果,这点同第一点。
———————————————————————————————————————————————————————————
CUDA则需要做cublas和curand的初始化。
默认提供了更改当前GPU设备的函数,set_device()。
device更改的时候,会让当前cublas和curand无效,需要释放并且重新申请。
宏
common.hpp,如其名“通用”二字,我们可以将一些重要的通用宏放置其中。
这些宏如下:
// MACRO: Instance a class
// more info see http://bbs.csdn.net/topics/380250382
#define INSTANTIATE_CLASS(classname) \
template class classname<float>; \
template class classname<double> // instance for forward/backward in cu file
// note that INSTANTIATE_CLASS is meaningless in NVCC complier
// you must INSTANTIATE again
#define INSTANTIATE_LAYER_GPU_FORWARD(classname) \
template void classname<float>::forward_gpu( \
const vector<Blob<float>*>& bottom, \
const vector<Blob<float>*>& top); \
template void classname<double>::forward_gpu( \
const vector<Blob<double>*>& bottom, \
const vector<Blob<double>*>& top); #define INSTANTIATE_LAYER_GPU_BACKWARD(classname) \
template void classname<float>::backward_gpu( \
const vector<Blob<float>*>& top, \
const vector<bool> &data_need_bp, \
const vector<Blob<float>*>& bottom); \
template void classname<double>::backward_gpu( \
const vector<Blob<double>*>& top, \
const vector<bool> &data_need_bp, \
const vector<Blob<double>*>& bottom) #define INSTANTIATE_LAYER_GPU_FUNCS(classname) \
INSTANTIATE_LAYER_GPU_FORWARD(classname); \
INSTANTIATE_LAYER_GPU_BACKWARD(classname)
★宏
宏的作用在第壹章已做详解。
实现
大部分简短实现都和成员函数声明写在了一起。
这里完善一下Dragon管理器的构造和析构函数。
#ifdef CPU_ONLY
// implements for CPU Manager
Dragon::Dragon():
mode(Dragon::CPU), solver_count(), root_solver(true) {}
Dragon::~Dragon() { }
void Dragon::set_device(const int device_id) {}
#else
// implements for CPU/GPU Manager
Dragon::Dragon() :
mode(Dragon::CPU), solver_count(), root_solver(true),
cublas_handle(NULL), curand_generator(NULL){
if (cublasCreate_v2(&cublas_handle) != CUBLAS_STATUS_SUCCESS)
LOG(ERROR) << "Couldn't create cublas handle.";
if (curandCreateGenerator(&curand_generator, CURAND_RNG_PSEUDO_DEFAULT) != CURAND_STATUS_SUCCESS
|| curandSetPseudoRandomGeneratorSeed(curand_generator, cluster_seedgen()) != CURAND_STATUS_SUCCESS)
LOG(ERROR) << "Couldn't create curand generator.";
} Dragon::~Dragon(){
if (cublas_handle) cublasDestroy_v2(cublas_handle);
if (curand_generator) curandDestroyGenerator(curand_generator);
}
构造与析构
CPU和GPU用宏隔开编译了,默认模式是CPU,solver_count为1,root_sovler为真
GPU的构造和析构函数还需要追加cublas和curand的初始化和释放。
void Dragon::set_device(const int device_id) {
int current_device;
CUDA_CHECK(cudaGetDevice(¤t_device));
if (current_device == device_id) return;
// The call to cudaSetDevice must come before any calls to Get, which
// may perform initialization using the GPU. // reset Device must reset handle and generator???
CUDA_CHECK(cudaSetDevice(device_id));
if (Get().cublas_handle) cublasDestroy_v2(Get().cublas_handle);
if (Get().curand_generator) curandDestroyGenerator(Get().curand_generator);
cublasCreate_v2(&Get().cublas_handle);
curandCreateGenerator(&Get().curand_generator, CURAND_RNG_PSEUDO_DEFAULT);
curandSetPseudoRandomGeneratorSeed(Get().curand_generator, cluster_seedgen());
}
当CUDA人工强制切换设备后(通常不建议这么做),原有的cublas句柄和curand发生器会失效。
因为它们是绑定GPU的,这时候需要销毁重新构造,绑定新的GPU。
另外,在Windows上,短时间内关闭打开,多次启动程序,绑定cublas句柄频率过快,也可能导致绑定失败。
完整代码
common.hpp:
https://github.com/neopenx/Dragon/blob/master/Dragon/include/common.hpp
common.cpp:
https://github.com/neopenx/Dragon/blob/master/Dragon/src/common.cpp
从零开始山寨Caffe·叁:全局线程管理器的更多相关文章
- 从零开始山寨Caffe·肆:线程系统
不精通多线程优化的程序员,不是好程序员,连码农都不是. ——并行计算时代掌握多线程的重要性 线程与操作系统 用户线程与内核线程 广义上线程分为用户线程和内核线程. 前者已经绝迹,它一般只存在于早期不支 ...
- 从零开始山寨Caffe·捌:IO系统(二)
生产者 双缓冲组与信号量机制 在第陆章中提到了,如何模拟,以及取代根本不存的Q.full()函数. 其本质是:除了为生产者提供一个成品缓冲队列,还提供一个零件缓冲队列. 当我们从外部给定了固定容量的零 ...
- 从零开始山寨Caffe·拾:IO系统(三)
数据变形 IO(二)中,我们已经将原始数据缓冲至Datum,Datum又存入了生产者缓冲区,不过,这离消费,还早得很呢. 在消费(使用)之前,最重要的一步,就是数据变形. ImageNet Image ...
- 从零开始山寨Caffe·陆:IO系统(一)
你说你学过操作系统这门课?写个无Bug的生产者和消费者模型试试! ——你真的学好了操作系统这门课嘛? 在第壹章,展示过这样图: 其中,左半部分构成了新版Caffe最恼人.最庞大的IO系统. 也是历来最 ...
- 从零开始山寨Caffe·零:必先利其器
工作环境 巧妇有了米炊 众所周知,Caffe是在Linux下写的,所以长久以来,大家都认为跑Caffe,先装Linux. niuzhiheng大神发起了caffe-windows项目(解决了一些编译. ...
- 从零开始山寨Caffe·壹:仰望星空与脚踏实地
请以“仰望星空与脚踏实地”作为题目,写一篇不少于800字的文章.除诗歌外,文体不限. ——2010·北京卷 仰望星空 规范性 Caffe诞生于12年末,如果偏要形容一下这个框架,可以用"须敬 ...
- 从零开始山寨Caffe·贰:主存模型
你左手是内存,右手是显存,内存可以打死显存,显存也可以打死内存. —— 请协调好你的主存 从硬件说起 物理之觞 大部分Caffe源码解读都喜欢跳过这部分,我不知道他们是什么心态,因为这恰恰是最重要的一 ...
- 从零开始山寨Caffe·拾贰:IO系统(四)
消费者 回忆:生产者提供产品的接口 在第捌章,IO系统(二)中,生产者DataReader提供了外部消费接口: class DataReader { public: ......... Blockin ...
- 从零开始山寨Caffe·伍:Protocol Buffer简易指南
你为Class外访问private对象而苦恼嘛?你为设计序列化格式而头疼嘛? ——欢迎体验Google Protocol Buffer 面向对象之封装性 历史遗留问题 面向对象中最矛盾的一个特性,就是 ...
随机推荐
- Arcgis SDE10.1 和 Arcgis server10.1的授权文件license
把下面内容复制进空白文本文档,改名和后缀为sde.ecp即可. 3dengine,101,ecp.arcgis.engine,none,WEJDFAZAM5FBAZ8LN115 3dserver,10 ...
- 深入了解C#系列:谈谈C#中垃圾回收与内存管理机制
今天抽空来讨论一下.Net的垃圾回收与内存管理机制,也算是完成上个<WCF分布式开发必备知识>系列后的一次休息吧.以前被别人面试的时候问过我GC工作原理的问题,我现在面试新人的时候偶尔也会 ...
- NodeJs框架
Node.js非常适用于Web开发,但是现在无论是一个网站,还是Web App都已经成为包括很多不同部分,如前端.数据库.业务模块.功能模块等等的大型项目,使用Node.js从零开始进行Web开发,也 ...
- 适配iOS10以及Xcode8
现在在苹果的官网上,我们已经可以下载到Xcode8的GM版本了,加上9.14日凌晨,苹果就要正式推出iOS10系统的推送了,在此之际,iOS10的适配已经迫在眉睫啦,不知道Xcode8 beat版本, ...
- Android 四大组件之Service
---恢复内容开始--- 1,Service的生命周期
- 【Unity3D】利用Shader以及更改Mesh实现2D游戏的动态阴影效果
最近看到一个非常有趣的益智小游戏,是一个盗贼进入房子偷东西的, 其实这种游戏市面上已经很多了,吸引我的是那个类似手电筒的效果, 主角走到哪里,光就到哪里,被挡住的地方还有阴影.有点类似策略游戏里的战争 ...
- PHP慢脚本日志和Mysql的慢查询日志
1.PHP慢脚本日志 间歇性的502,是后端 PHP-FPM 不可用造成的,间歇性的502一般认为是由于 PHP-FPM 进程重启造成的. 在 PHP-FPM 的子进程数目超过的配置中的数量时候,会出 ...
- tyvj1097 mm不哭
背景 Bless all rp++.. 描述 在一个数轴上,有n个MM(绝非恐龙!)在哭泣(5555~一直哭). tcboy也在这个数轴上,并恰好看到了这一幕,由于每个MM哭都会让tcboy损失一定的 ...
- ASP.NET AJAX调用 WebService
同事的代码,帮忙修改的,为了实现页面跳转回来后,状态的保持,Service 使用了Session. 主要的JS $.ajax({ url: "/ws/StaffInfo.asmx/Note& ...
- druid 配置
连接池配置,以及相关配置 <bean id="logFilter" class="com.alibaba.druid.filter.logging.Slf4jLog ...