神威太湖之光 样例代码分析
Example 1: MPI-Athread Clang
master.c:
定义计算规模
1 | #define J 64 |
声明从核函数
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 | static inline unsigned long rpcc() |
这是一段内联汇编的炫技代码,看看就好了,反正申威也没给指令手册
初始化MPI环境
跳过一段变量的定义,看到main函数里
1 | int main(int argc, char **argv) |
主核输出信息,并刷新缓冲区保证文字能被显示
1 | int main() { ... |
初始化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 | int main() { ... |
并行计算部分
好了,关键的部分开始了
- 初始化Athread库
- 使用从核进行计算
- 等待从核计算完毕
1 | int main() { ... |
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 | int main() { ... |
slave.c
变量声明
1 | __thread_local volatile unsigned long get_reply, put_reply; |
声明
volatile
的变量可能指向一个随时都能被计算机系统其他部分修改的地址,例如一个连接到中央处理器的设备的硬件寄存器,上面的代码永远检测不到这样的修改。如果不使用volatile
关键字,编译器将假设当前程序是系统中唯一能改变这个值部分(这是到目前为止最广泛的一种情况)。 为了阻止编译器像上面那样优化代码,需要使用volatile
关键字 更多信息
__thread_local
指把变量扔到LDM里(Scratch Pad Memory) 更多信息
1 | extern double a[J][I], b[J][I], c[J][I]; |
在
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 mode
DMA传输命令模式
;void *src
DMA传输主存源地址
;void *dest
DMA传输本地局存目标地址
;int len
DMA传输数据量
,以字节为单位;void *reply
DMA传输回答字地址
,必须为局存地址,地址4B对界;char mask
DMA传输广播有效向量
,有效粒度为核组中一行
,某位为1
表示对应的行传输有效
,作用于广播模式
和广播行模式
;int stride
主存跨步
,以字节为单位;int bsize
行集合模式下,必须配置,用于指示在每个运算核心上的数据粒度大小
;其它模式下,在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 | void func() { ... |
这里没啥好看的
把计算结果传回主存
1 | void func() { ... |
athread_put
运算核心局存LDM
往主存MEM
发送数据,不支持广播模式
和广播行模式
参数说明:
dma_mode mode
DMA传输命令模式
;void *src
DMA传输主存源地址
;void *dest
DMA传输本地局存目标地址
;int len
DMA传输数据量
,以字节为单位;void *reply
DMA传输回答字地址
,必须为局存地址,地址4B对界;char mask
DMA传输广播有效向量
,有效粒度为核组中一行
,某位为1
表示对应的行传输有效
,作用于广播模式
和广播行模式
;int stride
主存跨步
,以字节为单位;int bsize
行集合模式下,必须配置,用于指示在每个运算核心上的数据粒度大小
;其它模式下,在DMA跨步传输时有效,表示DMA传输的跨步向量块大小,以字节为单位。
这参数表其实是一样的,put_reply
,因为只有一次DMA操作,所以只需要等待到其值为1的时候
编译命令
1 | sw5cc -host -I/usr/sw-mpp/mpi2/include -c master.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 | extern "C" { |
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 | sw5CC -host -I/usr/sw-mpp/mpi2/include -c master.cpp |
注意一个坑爹的地方,在神威上,cc指C语言编译器,CC是C++的编译器,嗯就是大小写的区别
提交命令
一样一样的,就是这次要运行example-cpp文件而不是example-c
Example 4: MPI-Athread Allshare
Example 3是Fortran,暂时跳过不看
master.c
定义数据规模
1 |
声明变量和接口
1 | double a1[J][I], b1[J][I], c1[J][I], cc1[J][I]; |
分配&初始化内存空间
1 | int main() { ... |
size1 = 512M
sizeof(ulong) = 8Byte
a, b, c, d每个分配512M*8B=4GB的内存,属实嚣张
1 | int main() { ... |
a, b, c, d全部初始化为1, 2, 3, 4, 然后sum_total
统计4个数组总共有多少个变量,除4就意味着1个数组有多少个变量
1 | int main() { ... |
不太清楚为什么要比较sum_total
和size1
,这两个应该是一样的啊,否则就Rumtime Error了
等待其他MPI线程完成初始化
1 | int main() { ... |
MPI_Barrier
阻塞线程,直到Communicator范围内所有线程都执行完Barrier前的程序更多信息
说起来你可能不信,下面的程序和Example 1真的是一毛一样,也就是说创建的abcd数组没有任何luan用,这个程序大概就是告诉你如何开这么大的内存空间
Example 5: MPI-Athread Allshare-Master
不说了,这个其实就是给MPI第0线程开了个比其他线程都大的数组,其他和Example 4都是一样的