Who are you?

MIC-基本语法

MIC并没有单独的编程语言,MIC编程是对C/C++/Fortran语言的扩展,其使用了编译制导语句。它十分类似于OpenMP,不过MPSS(Intel MIC Platform Software Stack,英特尔MIC平台软件栈)也提供了一些高级API函数接口以便满足不同需求。这篇文章主要对offload模式的MIC编程语法进行一个简单学习。

offload模式

关键词offload的作用:在offload作用范围内的程序代码是要在MIC卡上运行的。offload语句用于CPU与MIC的主从模式。

offload基本语句

1
2
3
4
5
6
7
8
9
C/C++:
#pragma offload
Fortran:
!dec$ OFFLOAD //后面必须紧跟OpenMP语句或是函数调用语句
!DIR$ OFFLOAD BEGIN ……
!除了OpenMP以外的其他语句,如do,call语句 //不能使用OpenMP语句
!DIR$ END OFFLOAD

SIMD模型:如果一个程序的主要代码集中在for循环中,那么他就是典型的SIMD,即每次循环迭代,都拥有相同的指令,只是数据各不相同。

Tips:

  • 移植到MIC上,只需加上上面的编译制导语句
  • 编译时不需要任何特殊的编译选项,默认为MIC程序,若要编译成CPU程序,则需要加入-no-offload
  • 普通的循环程序代码加上offload语句后,其在MIC上仍然是串行执行的(只用到了MIC一个核的一个硬件线程)。offload语句本身只是起到了指示编译器将代码放入设备端运行的作用,并不指示代码并行执行。若想实现在MIC上的并行,则需要在设备端使用OpenMP。

程序移植所需工作

  • 移植到MIC上,只需加上上面的编译制导语句
  • 编译时不需要任何特殊的编译选项,默认为MIC程序,若要编译成CPU程序,则需要加入-no-offload
  • 普通的循环程序代码加上offload语句后,其在MIC上仍然是串行执行的(只用到了MIC一个核的一个硬件线程)。offload语句本身只是起到了指示编译器将代码放入设备端运行的作用,并不指示代码并行执行。若想实现在MIC上的并行,则需要在设备端使用OpenMP。

Tips:判断程序是否在MIC执行:MIC提供了一个检查宏_MIC_(相关语法在后面涉及)

1
2
3
4
5
6
7
8
9
C/C++:
_attribute_((target(mic)))void funcheck(int i)
{
#ifdef_MIC_
printf("Index on MIC:%d\n",i);
#else
printf("Index on CPU:%d\n",i);
#endif
}

MIC数据传输

Tips

  1. 不建议在设备端使用打印或是输出语句
  2. 使用引语编程的一个好处便是使用一套代码就可以满足不同模型,只要不加上相应的编译选项,就不会使用该特性

关键字

  1. out:通知编译器,括号内的变量/数组是需要输出的,这样驱动就会在代码离开MIC卡时,将变量拷贝到内存中相应的位置中。

    注意

    • 若是在栈上声明的数组,其长度在编译时就已经确定,所以不需要在传输时标注长度,但是若是在堆上声明的数组,由于编译时大小不能确定,因此必须在传输时标注数组大小。关于堆栈的内容可以参考我的另外一篇博文:C/C++中堆与栈简析
    • 非数组的变量若不显式传值,一旦MIC用到,就会自动以inout方式传递。
  2. in:输入,在设备端开辟空间并将主机端数据复制到设备端

  3. inout:输入并输出,在设备端开辟空间,在进入设备端时将数据复制到设备端,在从设备端离开时,将数据从设备端复制到主机端。

  4. nocopy:不拷贝。仅在设备端建立空间,不复制数据。但是需要在CPU端声明

    1
    2
    _attribute_((target(mic)))float* a; //a是中间变量,因此不需要再CPU端申请空间,但是必须声明为全局变量,且加上attribute前缀。
    #pragma offload target(mic:0)nocopy(a:length(LEN) alloc_if(1) free_if(0)) //示例

    Tips:nocopy使用场合

    • 在不同的offload区域(offload两次),如果后一次需要用到的前一次计算后的某些变量和数据,则可以使用nocopy避免数据内存中转,造成浪费(#pragma offload nocopy(a)
  • 变量作为设备代码段中的临时变量(不需从主机赋初值,也不需传回主机)

Rule of thumbs

一般规则

  1. 传输关键字可以有多个或零个,当多个时:可以连续书写,也可以用逗号或空格隔开。相同的关键字可以在一个offload语句中出现多次,但相同的变量名不可以在一个offload中出现多次(即使是在不同的关键字中);
  2. 传输关键字后跟括号,括号内为变量名;
  3. 变量应为数组名或是指针(特指动态数组指针)或普通标量,多个变量之间用逗号隔开;
  4. 变量为指针时,指针只能指向非指针变量,即不支持二维指针
  5. 变量为数组或是指向数组的指针时,可以指定数组起始与长度
  6. 变量为指针时,需要在变量名后面加上”:length(len)”,其中len为动态数组的元素个数,若多个动态数组长度相同,则可以写在一起,如:in(a,b,c:length(20))。元素个数可以是变量
  7. 除了length以外,还有alloc_if、free_if、align、alloc、into等五个关键字,一个传输关键字用一个冒号即可
  8. alloc_if和free_if的参数是判断表达式,其计算结果应该是布尔型。若alloc_if的参数结果为真则在进入设备端是为前述变量开辟空间,若free_if的参数结果为真则在离开设备端时为前述变量释放空间。
  9. align的参数时一个正整数,且必须是2的正整数次幂,其含义为在设备端开辟的前述变量,以align参数的长度对齐
  10. alloc的参数是数组名,含义是创建指定的部分内存空间
  11. into的参数是变量或是数组名,但只能一对一传递,含义为将数组中主机端拷贝到设备端的另一个数组,或相反。into可以和alloc,alloc_if,free_if结合使用,但不能与inout,nocopy同时使用。

示例讲解:P92

in/out/inout的实用语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 使用in out inout 传递部分数组 */
1 typedef int Array[10][10];
2 int a[1000][500];
3 int *p;
4 Array *q;
5 int *r[10][10]
6 int i,j;
7 struct { int y; } x;
8 #pragma offload ... in(a);
9 #pragma offload ... out(a[i:j][:])
10 #pragma offload ... in(p[0:100])
11 #pragma offload ... in((*q)[5][:])
12 #pragma offload ... out(x,y)
  • 8:最常用的数组引用方法,传输全部数据

  • 9:传输数组a的一部分,其中[i:j]规范第一维,i表示起始位置,j表示个数,第二维中只有冒号,省略了前后,表示第二维是完整的。长度参数可以是变量

  • 10:即使传输的是指向动态数组的指针,也可以用数组的“[ ]”表示。

  • 11:第一维只有一个参数5,意味第一维只有一个元素

  • 12:表示可以传输结构体的一部分

    注意:需要注意的是,虽然传输的是数组的一部分,但在MIC卡端开辟内存空间时,仍然开辟了从第一个元素开始的全部空间,所以一方面这种写法并没有减少内存使用,另一方面在使用时仍然要将数组视为整体使用

    针对此种写法的关键字:alloc和into

    • 由于传输部分数组的语法会开辟全部的内存空间,因此可以使用alloc语法限定开辟的空间范围

      1
      2
      3
      4
      /* 下述语句:
      * ①在设备端开辟1000个元素的数组p,数组下标从5开始,到1004;
      * ②将主机端的p[10]-p[109]传到设备端的p[10]-p[109],检查数据越界的责任仍然是程序员。 */
      pragma offload ... in (p[10:100]:alloc(p[5:1000])
    • into语句可以将主机端的数据数组一部分传递给另一个设备数组

      1
      2
      /* 使用这种方式需要注意数组覆盖的情况。另外还要注意的就是into不能实现不同维度数组间的数据传递。*/
      #pragma offload ... in (p[0:500]:into(p1[500:500]))

其他一些关键字

target

  1. target(mic):mic是目前唯一合法的取值。在运行时可以指定使用第几块mic卡,用法是在mic后加冒号并附加序号,如target(mic:1),序号应注意:

    • 当序号大于等于0时,程序将offload到相应设备设备上,设备号计算方法:设备号=序号mod总设备数(取余)
    • 当序号等于-1时,系统自动选择计算设备
    • 序号不可小于-1
  2. 使用多块MIC卡协同计算的OpenMP循环代码

    1
    2
    3
    4
    5
    6
    7
    omp_set_nested(1); //嵌套OpenMP代码
    #pragma parallel for num_threads(3) //假设有两块MIC卡,和CPU协同计算
    for(i=0;i<3;i++)
    {
    #pragma offload target(mic:i) if(i>1) in(...) out(...)
    ...
    }

    以上代码将第一份任务指定给了CPU,第2,3份指定给了两块不同的MIC卡。由于使用了嵌套OpenMP代码,因此需要调用第一句。

  3. 在offload之前,可以使用API函数 int\_Offload\_number\_of\_devices(void)确定系统拥有的MIC设备数。在offload的代码段里,可以使用API函数int\_Offload\_get\_device\_number(void)获取该代码段所在的设备编号。

if

根据条件判断是否要将代码段放到设备上运行,若表达式为真,则在MIC端运行,否则在CPU端运行(如上代码)

mandatory

  1. 表示该代码必须在MIC上运行,若设备不可用,直接报错。
  2. 不可与if同时使用。

异步传输

  1. signal和wait:即CPU端无需等待offload语句返回,即可异步运行下面的代码。一般用于启动MIC代码段后,并发执行CPU代码,达到同步执行的目的
  2. offload_transfer和offload_wait:与offload类似,只负责数据传输,后面不加入计算代码。其中offload_transfer支持的参数与offload语句相同,但offload只支持target、if、wait三个参数
  3. 用法:
    • signal语句在offload语句代码段结束后发送一个信号,wait语句负责接收,所以二者一定是成对使用
    • wait语句可以一次等待多个信号,所以二者语句数量未必相等
    • signal 和 wait的参数是tag,在C中,是数组指针,同时传输多个数组指针时,能且只能signal/wait一个数组名。在Fortran中,该变量是一个整型变量。

offload语法总结

1
2
/* 使用格式 */
#pragma offload specifier[,specifier ...]

其中,specifier可填入:target,if,in,out,inout,nocopy,signal,wait,mandatory,而在in/out/inout/nocopy可用属性有:length,alloc_if,free_if,align,alloc,into

变量与函数声明

单个声明方式

1
2
3
4
5
6
7
C/C++:
_declsspec(target(mic))函数或变量声明
_attribute_((target(mic)))函数或变量声明
Fortran:
!DIR$ attributes offload: target-name :: routine 名或变量名[,routine 名或变量名]...

批量声明方式

1
2
3
4
5
6
7
8
9
10
11
12
13
C/C++:
#pragma offload_attribute(push, target(mic))
变量或函数声明;
#pragma offload_attribute(pop)
#pragma offload_attribute( target(mic))
变量或函数声明;
#pragma offload_attribute( target(none))
Fortran:
!DIR$ OPTIONS /OFFLOAD_ATTRIBUTE_TARGET=mic
变量或函数声明;
!DIR$ END OPTIONS

注意

  • 在C/C++中,使用attribute的话,需要注意target外围是两层括号,只写一层是错的
  • 上述声明方式适用于函数与变量,变量是全局变量
  • 函数和变量可以同时用于CPU与MIC

MIC程序编译运行注意事项

头文件

  1. 在C/C++中,在MIC中若要用到API,需包含offload.h
  2. 在Fortran中,需要include mic_lib.f90或USE mic_lib

环境变量

  1. MIC_STACKSIZE:规定MIC上每个线程所占的栈的大小,默认2MB,可以修改:

    1
    export MIC_STACKSIZE=5M
  2. MIC_ENV_PREFIX:可以设置MIC专属环境变量的前缀,以区分MIC端和CPU端的环境变量,若不加以却别,默认情况下,CPU端的环境变量会作用于MIC端,示例:

    1
    2
    3
    export OMP_NUM_THREADS=8
    export MIC_OMP_NUM_THREADS=124
    export MIC_ENV_PREFIX=MIC_

    使用方法如下:定义一个前缀“MIC_”,若MIC上有环境变量符合前缀,则去掉前缀,并将其作用于MIC上。

  3. MIC_LD_LIBRARY_PATH:定义了程序在MIC上运行时所需要用到的共享库路径,此处的共享库路径指MIC卡上的地址,即需要手动将共享库传输到MIC端,路径一般指向用户自己编写的共享库。由于主机端的LD_LIBRARY_PATH不会自动扩展到MIC上,因此不需要结合上面的环境变量

编译选项

  1. -mmic:表示编译出的程序只能在MIC上运行。默认不开启,不开启时编译产生的程序是异构程序。
  2. -no-offload:表示编译出的程序只能在CPU上运行,使用这个选项时,将忽略程序中所有与MIC相关的语句。默认不开启,开启时编译产生的程序是纯CPU程序。
  3. -offload-attribute-target=mic:表示所有未声明为mic可用的函数和全局变量都可以在MIC中使用,与在代码中使用attribute属性声明函数或变量的效果相同。默认不开启。
  4. -offload-option,target,tool,"option-list":作用为针对被offload的对象,即在MIC卡上运行的代码段,使用特殊的编译选项。其中target只能选mic,tool只能取ld(链接库时用到的程序),as(汇编器)或compiler(编译器)之一,option-list是具体的选项,必须用引号括起来,选项之间用空格分开。

注意

  • MIC程序的编译选项与CPU程序无多大差别,绝大多数的CPU程序编译选项也能在MIC程序中使用
  • 异构程序编译时,使用的编译选项会全部传递给主机端编译程序。有一部分(MIC支持的部分)会被传递给MIC端编译程序,可以通过"-watch=mic-cmd"查看
  • 使用offload-option(上述第4点)增加的编译选项,只能在MIC端使用,并且会覆盖或附加在CPU端过去的编译选项之后。若想覆盖,则需要将-offload-load写在自动传输的参数前面,若-offload-option选项被写在后面,则会附加到自动传输的参数后面。

其他问题

  • 在offload区域只能正常退出,使用exit()会报错
  • 在Fortran中在外部函数/subroutine中定义的变量不能通过offload语句传递给内部/subroutine,但是SAVE变量不受约束。

参考资料:

  • MIC高性能计算编程指南,王恩东,中国水利水电出版社,2012