神威太湖之光 样例代码分析

Example 1: MPI-Athread Clang

master.c:

定义计算规模

1
2
#define J 64
#define I 1000

声明从核函数

1
extern SLAVE_FUN(func)();

这一句对应slave.c中的

1
2
3
4
5
6
7
8
void func() {...} ```

### 创建数组

```c
double a[J][I], b[J][I], c[J][I], cc[J][I];
double check[J];
unsigned long counter[J];

因为这个程序运行在主核上,声明变量无修饰,故该数组实际存放在用户共享连续空间中更多信息

获取当前时间

1
2
3
4
5
6
7
8
static inline unsigned long rpcc()
{
unsigned long time;
asm("rtc %0"
: "=r"(time)
:);
return time;
}

这是一段内联汇编的炫技代码,看看就好了,反正申威也没给指令手册

初始化MPI环境

跳过一段变量的定义,看到main函数里

1
2
3
4
5
6
7
8
int main(int argc, char **argv)
{
...
int myid, numprocs;

MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);

主核输出信息,并刷新缓冲区保证文字能被显示

1
2
3
4
5
6
int main() { ...
if (myid == 0)
{
printf("!!!!!! C-EXAMPLE TOTAL PROCS is %d !!!!!!\n", numprocs);
fflush(NULL);
}

初始化a和b数组

master.c中定义全局变量

1
2
3
4
5
6
7
8
9
10
double a[J][I], b[J][I], c[J][I], cc[J][I]; ```

```c
int main() { ...
for (j = 0; j < J; j++)
for (i = 0; i < I; i++)
{
a[j][i] = (i + j + 0.5);
b[j][i] = (i + j + 1.0);
}

串行计算,并统计时间

1
2
3
4
5
6
7
8
int main() { ...
st = rpcc();
for (j = 0; j < J; j++)
for (i = 0; i < I; i++)
{
cc[j][i] = (a[j][i]) / (b[j][i]);
}
ed = rpcc();

并行计算部分

好了,关键的部分开始了

  • 初始化Athread库
  • 使用从核进行计算
  • 等待从核计算完毕
1
2
3
4
5
6
7
8
9
int main() { ...
athread_init();

st = rpcc();

athread_spawn(func, 0);
athread_join();

ed = rpcc();

athread_spawn创建线程组

参数说明: start_routine fpc函数指针 void * arg函数 f 的参数起始地址

因为fun没有参数,干脆指定个0

athread_join等待线程组终止

slave.c中, func的定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void func()
{
...
my_id = athread_get_id(-1);
athread_get(...&a[my_id][0], &a_slave[0]...);
athread_get(...&b[my_id][0], &b_slave[0]...);
...
for (i = 0; i < I; i++)
{
c_slave[i] = a_slave[i] / b_slave[i];
}
...
athread_put(...&c_slave[0], &c[my_id][0]..);
} ```

很显然,计算的工作被全部抛给了从核

### 计算Checksum

```c
int main() { ...
checksum = 0.0;
checksum2 = 0.0;
for (j = 0; j < J; j++)
for (i = 0; i < I; i++)
{
checksum = checksum + c[j][i];
checksum2 = checksum2 + cc[j][i];
}

关闭MPI Athread环境

1
2
3
4
int main() { ...
athread_halt();
MPI_Finalize();
}

slave.c

变量声明

1
2
3
4
5
6
__thread_local volatile unsigned long get_reply, put_reply;
__thread_local volatile unsigned long start, end;

__thread_local int my_id;

__thread_local double a_slave[I], b_slave[I], c_slave[I];

声明volatile的变量可能指向一个随时都能被计算机系统其他部分修改的地址,例如一个连接到中央处理器的设备的硬件寄存器,上面的代码永远检测不到这样的修改。如果不使用volatile关键字,编译器将假设当前程序是系统中唯一能改变这个值部分(这是到目前为止最广泛的一种情况)。 为了阻止编译器像上面那样优化代码,需要使用volatile关键字 更多信息

__thread_local指把变量扔到LDM里(Scratch Pad Memory) 更多信息

1
2
extern double a[J][I], b[J][I], c[J][I];
extern unsigned long counter[64];

master.c中声明了a,b,c数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
double a[J][I], b[J][I], c[J][I], cc[J][I];
double check[J];
unsigned long counter[J]; ```

[Extern关键字的更多信息](https://zh.wikipedia.org/wiki/%E5%A4%96%E9%83%A8%E5%8F%98%E9%87%8F)


### 从主存中分割数据并储存到LDM

```c
void func()
{
int i, j;

my_id = athread_get_id(-1);

get_reply = 0;
athread_get(PE_MODE, &a[my_id][0], &a_slave[0], I * 8, &get_reply, 0, 0, 0);
athread_get(PE_MODE, &b[my_id][0], &b_slave[0], I * 8, &get_reply, 0, 0, 0);

while(get_reply!=2);

athread_get_id获得线程逻辑标识号

athread_get运算核心局存LDM接收主存MEM数据

参数说明: dma_mode modeDMA传输命令模式void *srcDMA传输主存源地址void *destDMA传输本地局存目标地址int lenDMA传输数据量,以字节为单位; void *replyDMA传输回答字地址,必须为局存地址,地址4B对界; char maskDMA传输广播有效向量,有效粒度为核组中一行,某位为1表示对应的行传输有效,作用于广播模式广播行模式int stride主存跨步,以字节为单位; int bsize行集合模式下,必须配置,用于指示在每个运算核心上的数据粒度大小;其它模式下,在DMA跨步传输时有效,表示DMA传输的跨步向量块大小,以字节为单位。

关于DMA模式的更多信息

这里可以看出来,DMA传输用了最简单的PE_MODE,也就是自己单干,传输的时候不管其他从核的模式,只把主存上的两行数据(a, b各一行)复制到LDM上。

I*8,其实就是因为a, b是double类型的,double是64bit长,也就是8byte长,I*8就是一行double(I个变量)实际占用的空间(byte为单位)

最后一句的get_reply,实际上是DMA传输完成后会给get_reply自动加上1,这个while循环就是在等待DMA传输完成

计算部分

1
2
3
4
void func() { ...
for(i=0;i<I;i++){
c_slave[i]=a_slave[i]/b_slave[i];
}

这里没啥好看的

把计算结果传回主存

1
2
3
4
5
void func() { ...
put_reply=0;
athread_put(PE_MODE,&c_slave[0],&c[my_id][0],I*8,&put_reply,0,0);
while(put_reply!=1);
}

athread_put运算核心局存LDM主存MEM发送数据,不支持广播模式广播行模式

参数说明: dma_mode modeDMA传输命令模式void *srcDMA传输主存源地址void *destDMA传输本地局存目标地址int lenDMA传输数据量,以字节为单位; void *replyDMA传输回答字地址,必须为局存地址,地址4B对界; char maskDMA传输广播有效向量,有效粒度为核组中一行,某位为1表示对应的行传输有效,作用于广播模式广播行模式int stride主存跨步,以字节为单位; int bsize行集合模式下,必须配置,用于指示在每个运算核心上的数据粒度大小;其它模式下,在DMA跨步传输时有效,表示DMA传输的跨步向量块大小,以字节为单位。

这参数表其实是一样的,put_reply,因为只有一次DMA操作,所以只需要等待到其值为1的时候

编译命令

1
2
3
sw5cc -host -I/usr/sw-mpp/mpi2/include -c master.c
sw5cc -slave -c slave.c
mpicc master.o slave.o -o example-c

注意主从核程序分开编译,然后用mpicc链接(ld乙烷)

提交运行

1
bsub -b -I -q q_sw_expr -n 2 -cgsp 64 -host_stack 256 -share_size 4096 ./example-c

-I选项表示提交交互式作业,使作业输出在作业提交窗口; -b表示从核函数栈变量放在从核局部存储上,该选项为获取加速性能必须的提交选项; -q向指定的队列中提交作业; -n指定需要的所有主核数; -cgsp指定每个核组内需要的从核个数,指定时该参数必须<=64; -share_size指定核组共享空间大小,一般最大可以用到7600MB; -host_stack指定主核栈空间大小,默认为8M,一般设置为128MB以上。

Example 2: MPI-Athread C++

master.c

其实这个与Example 1的区别不大

约定从核函数接口

1
2
3
4
5
extern "C" {
#include <athread.h>

void slave_func(void);
}

extern "C"的真实目的是实现类C和C++的混合编程。在C++源文件中的语句前面加上extern "C",表明它按照类C的编译和连接规约来编译和连接,而不是C++的编译的连接规约。这样在类C的代码中就可以调用C++的函数or变量等。 更多信息

并行计算部分

Example 1下面这句代码

1
2
3
4
5
6
athread_spawn(func, 0); ```

被替换成了

```c
__real_athread_spawn((void *)slave_func,0);

slave.c

因为从核只支持C语言,所以这里的slave.c文件内容其实和Example 1是一样的

编译命令

1
2
3
sw5CC -host -I/usr/sw-mpp/mpi2/include -c master.cpp
sw5cc -slave -c slave.c
mpiCC master.o slave.o -lstdc++ -o example-cpp

注意一个坑爹的地方,在神威上,cc指C语言编译器,CC是C++的编译器,嗯就是大小写的区别

提交命令

一样一样的,就是这次要运行example-cpp文件而不是example-c

Example 4: MPI-Athread Allshare

Example 3是Fortran,暂时跳过不看

master.c

定义数据规模

1
2
3
4
5
6
#define DIM_I 1024 
#define DIM_J 1024
#define DIM_K 512

#define J 64
#define I 1000

声明变量和接口

1
2
3
4
5
6
7
8
double a1[J][I], b1[J][I], c1[J][I], cc1[J][I];

int main(int argc, char *argv[])
{
unsigned long *a, *b, *c, *d;
unsigned long sum_total, size1;
...
extern void slave_func();

分配&初始化内存空间

1
2
3
4
5
6
int main() { ...
size1 = DIM_I * DIM_J * DIM_K;
a = (unsigned long *)malloc(size1 * sizeof(unsigned long));
b = (unsigned long *)malloc(size1 * sizeof(unsigned long));
c = (unsigned long *)malloc(size1 * sizeof(unsigned long));
d = (unsigned long *)malloc(size1 * sizeof(unsigned long));

size1 = 512M

sizeof(ulong) = 8Byte

a, b, c, d每个分配512M*8B=4GB的内存,属实嚣张

1
2
3
4
5
6
7
8
9
10
int main() { ...
for (i = 0; i < size1; i++)
{
a[i] = 1;
b[i] = 2;
c[i] = 3;
d[i] = 4;
sum_total = sum_total + a[i] + b[i] / 2 + c[i] / 3 + d[i] / 4;
}
sum_total = sum_total / 4;

a, b, c, d全部初始化为1, 2, 3, 4, 然后sum_total统计4个数组总共有多少个变量,除4就意味着1个数组有多少个变量

1
2
3
4
5
6
int main() { ...
if (sum_total == size1)
{
size1 = 4 * 8 * size1 / (1024 * 1024 * 1024); /* a/b/c/d size is 4GB(8*512*1024*1024)*/
printf("Process %d Memery size is more than: %dGB \n", myid, size1); /*total size is 4*4GB=16GB*/
}

不太清楚为什么要比较sum_totalsize1,这两个应该是一样的啊,否则就Rumtime Error了

等待其他MPI线程完成初始化

1
2
int main() { ...
MPI_Barrier(MPI_COMM_WORLD);

MPI_Barrier阻塞线程,直到Communicator范围内所有线程都执行完Barrier前的程序更多信息

说起来你可能不信,下面的程序和Example 1真的是一毛一样,也就是说创建的abcd数组没有任何luan用,这个程序大概就是告诉你如何开这么大的内存空间

Example 5: MPI-Athread Allshare-Master

不说了,这个其实就是给MPI第0线程开了个比其他线程都大的数组,其他和Example 4都是一样的