个人blog原址:<并行程序设计导论> 一些笔记与思考 ,后续基本都会同步到知乎~
这两天重新看了《并行程序设计导论》,以前看过编程范式里的一些泛型编程,对于编程模型有些浅薄的思路,趁机记录一下。有经验的大神可以直接跳到第四部分看看本鶸的一些思路。
一、背景介绍
计算机比较原始的三大领域:存储、通讯、计算。
几年前老大做了分布式存储,去年做了异步通信,最近他要做并行计算了。
于是我上周学着以前做过的MIT 6.824课设的做法给做了个demo,老大不满意,说这个模型不希望上来就考虑存储,要把最抽象的语义提炼出来。我想了想,这其实就是hadoop与MPI的关系。
同样作为分布式框架,MPI的想法是计算和存储分离的,而hadoop因为有hdfs,所以属于计算向存储的迁移。可以说后者是前者的演进,但是MPI更为通用。
我们的workflow里的算法工厂一直秉承着这样的设计思想:所有基本算法任务都是符合通用算法的,进而在上面做行为派生。那么在我们想设计一类基本算法任务(比如排序、合并、归约),它的最小需求集是什么?
带着这个问题我看着书写了如下笔记与思考。
二、本书纲要
我看的英文版,几大章节分别是:
Chapter 1 Why Parallel Computing?
Chapter 2 Parallel Hardware and Parallel Software
Chapter 3 Distributed-Memory Programming with MPI
Chapter 4 Shared-Memory Programming with Pthreads
Chapter 5 Shared-Memory Programming with OpenMP
Chapter 6 Parallel Program Development
可以看到,并行计算可以按照内存中的并行或者分布式机器上的并行进行划分。本书介绍的pthreads,OpenMP,还有先前从入门到放弃的cuda编程都是属于前者,而MPI hadoop spark都属于后者。
第六章里有非常实用的例子是分别用这两种类型的编程思想去拆解n体问题和树型搜索问题。
三、MPI基本笔记
1. 启动
MPI是纯数据消息传递的,所有节点执行相同的代码,由执行时附带的参数来指定全局个数:
// 编译由mpicc为C编译器包了一层
mpicc -g -Wall -o mpi_hello mip_hello.o
// 起4个一样的程序作为一个小并行集群
mpiexec -n 4 ./mpi_hello
并行的语句都会写在MPI_Init(&argc, &argv)和MPI_Finalize()之间。
2. Communicator
既然是基于消息传递,MPI_Comm就很重要了,它是内部的通信器,从它那可以那到全局整体个数comm_sz和自己是第几个的my_rank,全局Communicator可以通过MPI_COMM_WORLD拿到。my_rank范围是0 ~ comm_sz - 1。
3. Send and Recv
直接看两个程序如何做收发:
// called by process q
MPI_Send(send_buf_p, send_buf_sz, send_type, dest, send_tag, send_comm);
// called by process r
MPI_Recv(recv_buf_p, recv_buf_sz, recv_type, src, recv_tag, recv_comm, &status);
则q发送r可以收到的条件至少满足:
- recv_comm == send_comm
- recv_tag == send_tag
- dest == r
- src == q
收发消息的语义上两个重要的点:
- 点到点之间的消息保序,但是全局不保序。(这个最简单的做法大概是每个点之间建立一个连接就可以了,如果要连接池,则对同一个target要分配一个队列去管理来做到保序)
- MPI_Send()包括了buffer和block模式,应该就是我们的非阻塞与阻塞模式,而MPI_Recv()则总是等着的,所以这对于用户的编程要求其实很高,当然灵活度也很高,即你可能因为Recv不到东西而hang住,或者Send的东西没人收而会丢失。
4. collective communication
点对点发送只是一种简单的模式,还有没有其他模式呢?collective communication就是了。不太好翻译这个应该叫什么,意思是所有人都要参与计算、并最终由一个人去收集的通信模式(相比之下,send-recv的模式是point-to-point)。感觉现在才正式进入并行计算的用法。
我们先借用我们最熟悉的归约来做详解:
int MPI_Reduce(
void * input_data_p,
void * output_data_p,
int count,
MPI_Datatype data_type, // MPI内部的char,int之类
MPI_Op operator, // 内部提供了MAX SUM,还可以自行派生实现
int dest_process, // 大家计算完发给一个人去收集
MPI_Comm communicator
);
一些特点:
- 所有人都调用同样的collective函数(比如不能有人在这一步做reduce有人做recv);
- 所有人指定的这一轮的收集者必须是同一个;
- 虽然只有收集的那个人需要output,但大家都要传;
- send-recv模式有tag去指定,而collective communication没有,所以这里是通过communicator来寻找collect的节点们的对应关系的,并通过函数调用的顺序(而非参数本身)来决定结果。
其他类似的api还有:
MPI_Allreduce(): 这个跟reduce比没有dest_process,即每个人都可以收集结果数据;
MPI_Bcast(): 参数只有input,没有output,顾名思义,把input发给所有人;
5. data distribution
这个有点类似我们做cuda编程时的分thread、分block概念,对于一份数据,我们必须很清晰它可以如何被并行处理,分块方式有block partition、cyclic partition、block-cyclic partition,概念很简单不细介绍,重点介绍下这个api,可以自动为我们把数据分comm_sz份分发:
int MPI_Scatter(
void* send_buf_p,
int send_count, // 这个send_count可以不被comm_sz整除
MPI_Datatype send_type,
void* recv_buf_p,
int recv_count,
MPI_Datatype recv_type,
int src_process, // 由哪号节点分发
MPI_Comm comm
);
与之对称的功能就是MPI_Gather()了,对应的参数时dest_process,指定由哪个节点做收集。通过buf数组的索引可以拿到具体的要分发给某个节点/来自某个节点的数据。
四、一些想象
第三章的重要概念就介绍完了,以下是我基于这些外层所想象的一些实现,这些思考不一定都是对的。
1. communicator通信管理
这个communicator看上去可以得知所有节点的ip:port,有点像侵入式的服务发现,mpi自身是个框架很容易做这个事情,类似于workflow的client端的upstream。
2. MPI_Send()/MPI_Recv()的错误处理
对用户的编程要求是比较高的,但是框架可以做一些事情来发现Send的数据没人收或者等在Recv上卡住的事情。对于workflow来说可以是接收时设置receive_timeout,send没人接收更简单,错误会在callback()返回给用户。再长远点来说,如果我们接管了语言层面,或许还可以在用户编译阶段画出DAG提前分析出这样的错误情况。
3. DAG
其实所有的计算步骤都是可以画成DAG的,从DAG的依赖上就很清楚可以看出哪些步骤可以并行。workflow现在的异步任务调度也是通用的道理,只是目前只能算shared-memory的parallel programming,所以才需要进一步提取像MPI这样的接口来为用户封装更抽象的并行计算模型。
4. 泛型编程
以前看过一系列泛型编程的文章,提到C++对于泛型的进一步抽象,与MPI面对的问题是比较像的,另外加上google的mapreduce的paper也提到过。我在此总结了下是包括这三点:迭代器、operator、归约函数。
举个例子,以上的MPI_Reduce中接口都可以这样映射到C++中实现:
template<class DATA_ITERATOR, class DATA_TYPE, class OPERATOR>
int cpp_reduce(DATA_ITERATOR i_begin, // 由于数据不一定是数组遍历,
DATA_ITERATOR i_end, // 所以需要input_data的迭代器begin和end
DATA_ITERATOR o_begin,
/* DATA_ITERATOR o_end, */
DATA_TYPE data_default, // 基本的数据初始化类型
OPERATOR op, // 基本的MAX SUM,也可以自定义
int dest_process, // 大家计算完发给一个人去收集
Communicator *comm)
{
DATA_TYPE data = data_default;
DATA_ITERATOR in_iter = i_begin;
DATA_ITERATOR out_iter = o_begin;
while (in_iter != i_end)
{
data = op(*in_iter);
if (data == data_default)
return -1;
*out_iter = std::move(data);
out_iter++;
in_iter++;
}
return comm->send(dest_process, o_begin, out_iter);
}
// 对比看下
int MPI_Reduce(
void * input_data_p,
void * output_data_p,
int count,
MPI_Datatype data_type, // MPI内部的char,int之类
MPI_Op operator, // 内部提供了MAX SUM,还可以自行派生实现
int dest_process, // 大家计算完发给一个人去收集
MPI_Comm communicator
);
这么写完发现MPI有些接口是不太通用的,比如input_data和output_data的类型和个数都必须一致。这些在我们做自己的算法任务时必须去想清楚。
五、最后
事实进一步证明,欠下的技术债迟早都要还。这本书印象很深刻三年前就很喜欢,去夏威夷旅游的时候还非要带在身边,然而根本没有消化。其实这本书对于三年前的我来说还是太抽象了。
现在我比较适合看的书,必须能有一些定义引领我的思路,然后最重要是不要太具体,能给我留有充分的想象空间,让我去思考现有的东西如何实现,或者我是否还需要开发新的东西去实现这样的模型/机制。而这本书现在看就正正好。昨晚看了一宿兴奋得没睡。最近杂事多,只能草草记录。希望最近能抓紧把这本书消化完,充分想象并行计算。