8.4 TASK_REGION指令  BACKWARDFORWARD


在HPF中可通过将数据对象映射到处理器子集上并且增加一些声明以便允许不同的代码块在不同的处理器子集上同时执行来隐式解释任务并行。数据对象映射到处理器子集上是通过向处理器排列的子区域上进行分配完成的。通过使用ON指令指定处理器子集上的执行。本节引入了一个TASK_REGION指令以便允许用户隐式指定不相连的处理器可以同时执行代码块。

可使用TASK_REGION指令声明一个代码块满足下列约束集合。一个任务区域内的所有词法顺序上最外层的ON块必须具有一个RESIDENT属性来指示在它们中所访问的所有数据被映射到相应的活动处理器子集上。进一步讲,这两个ON块内的代码一定不能有相互冲突的I/O。在这些约束下,如果两个这种ON块在不相连的处理器子集上执行,则它们可以安全地同时执行。

8.4.1 TASK_REGION指令句法

一个任务区域是一个由两个结构化注释定界的单入口区域:

H815 task-region is !HPF$ TASK_REGION
            block
            !HPF$ END TASK_REGION 

不允许从任务区域外向任务区域内的控制转移。如果转移不是从一个ON块内开始,则允许将控制转移到任务区域外。(这么做的原因在后面是很显然的)

8.4.2 TASK_REGION指令的语义

我们称由一个TASK_REGION...END TASK_REGION对所封闭的代码段为一个任务区域。TASK_REGION指令为程序员提供了一种方法来声明一个代码区域满足一组条件。编译器期望使用这些代码来生成任务并行代码。

一个任务区域可包含被指导在ON处理器子集上执行的代码段。所有其它的代码在包含所有活动处理器的一个子集上执行。一个任务区域内的最外嵌套层的每个ON块(例如其它的ON块内或其它的任务区域内)被定义为一个词法任务。一个词法任务的每个执行实例被定义为一个执行任务且如果能从上下文清楚地区分开也可将其称为just task。

一个任务区域内必须满足下列约束:

8.4.3 执行模型和用法

任务区域没有引进一个根本的新执行模型。但是,隐含在任务区域内的这些声明暗示只有执行任务的指定活动处理器才需要参加它的执行,其它的处理器可以跳过它的执行。执行任务区域的处理器要参加在它所属的处理器子集上执行的所有任务的执行,它不参加那些不在它所属的处理器子集上执行的任务。词法任务之外的代码由所有的活动处理器作为普通的数据并行代码执行。任务区域的这些访问约束保证了这一执行模式所获得的结果与任务区域上纯粹的数据并行的结果相一致。

任务区域提出了一个简单且有效的模型来书写综合的任务和数据并行程序。我们阐述了三个基本的计算结构,在这些结构中可使用这个模型来有效地开发任务并行。

  1. 并行区域(parallel sections):可使用一个任务区域将可利用的处理器分成不相连的集合以执行不相关的计算,模拟通常所说的并行区域。任务并行的这种形式在许多应用方案中相当直接有用,例如多块应用。任务区域在不相连的处理器子集上简单地包含了RESIDENT ON块的一个序列。注意子集中处理器的划分可以是动态的,即,可根据执行过程中所计算的其它变量。
  2. 嵌套并行(nested parallelism):可将任务区域嵌套,特别是一个执行任务所产生的子程序调用可使用其它的任务区域指令进一步细分执行(或活动)处理器。这样就允许了嵌套并行的开发。例如动态树结构划分和征服计算的实现。作为一个具体的例子,quicksort的实现可通过围绕一个中心点递归地划分输入关键字数组并将均衡数目的处理器赋给两个作为划分结果而获得的新数组。
  3. 数据并行流水线(data parallel pipeline):可使用任务区域实现流水数据并行计算。我们将用一个二维快速傅里叶变换(2D FFT)计算阐述这一点。2D FFT的第一个阶段是读一个二维矩阵并在矩阵的每一行执行一个一维FFT。第二阶段在矩阵的每一列执行一个一维FFT并产生最后的输出。在2D FFT的这种形式的流水数据并行实现中,两个阶段被映射到不相连的处理器子集上。8.4.5节中包含了带有简要描述的2D FFT的任务和数据并行代码。
8.4.4 实现

任务区域只是关于代码块的一个简单声明,而且任务并行的开发至少部分依赖于编译方案。怎样开发任务并行的细节强烈依赖于并行系统结构,编译器,和根本的通信模型,我们将指出一些重要的考虑并用一个例子阐述任务并行代码的生成。我们首先阐述使用消息传递通信和同步模型的分布存储机器,但也将指出一些与共享存储相关的重要问题。

8.4.4.1 本地化计算和通信

除了相关ON子句中直接执行任务的那些处理器之外,执行任务内的计算和通信不应涉及任何其它的处理器。

在一个词法任务的入口,编译器必须插入检查以便不在相关子集中的处理器跳到紧接该任务的代码中。由于执行任务不能访问活动执行处理器子集之外的数据,因此在相关的执行处理器和其它处理器之间无须产生任何通信。在一个消息传递模型中,仅产生必要消息的通信产生算法将很自然地获得所需的结果。但是,一些通信方案可能涉及不通信的处理器间空消息的产生,而且保证一个执行任务的活动处理器和其它处理器之间不产生空消息是很重要的。使用栅障同步的通信模型(在共享或分布存储的机器中)必须保证执行任务内的所有栅障是仅跨越活动处理器的子集栅障。实现时还需要在执行任务的出口和入口包括一个子集栅障以保证执行任务内外数据访问的一致性。一般来说,编译框架必须保证执行任务内外数据访问的一致性,这一点可通过一个共享或分布存储环境内任何虚拟同步上下文做到。

8.4.4.2 重复计算

所有唯一包含重复变量的计算都应该在所有执行处理器上重复。一种简单的替代是由一个处理器执行计算然后将结果广播给所有的处理器。这种重复不但在HPF中一般来说很有利,而且由于广播所产生的通信可能导致干扰任务并行的额外同步,所以这种重复在任务区域中尤其重要。

8.4.4.3 I/O的涵义

在一些并行系统实现中,I/O是通过系统的单独的一个处理器来执行的。虽然不一定每个处理器都能物理上独立地执行所有的I/O操作,存在I/O的任务并行还是假定所有的处理器都能独立地执行I/O而且必须支持这种模式。一个简单的解决方法是有单一一个执行所有I/O的指定处理器,但不将其看作一个执行处理器,因此不会有任何相关依赖的执行。

8.4.4.4 SPMD或MIMD的代码生成

编译器的另一个问题是是否应该在所有的处理器上执行相同的代码映象。由于不同的处理器组可能需要不同的变量,因此纯粹的SPMD实现可能会浪费内存,原因是它必须在所有的处理器上分配所有的变量。可通过动态存储分配解决这个问题,但会增加复杂性开销。对于不同的处理器子集使用不同的代码映象是另一种解决方法,但这样会导致大量的额外复杂性。

8.4.5 2-D FFT的例子

本节使用任务并行来建立一个流水数据并行二维FFT,并通过显示HPF程序所产生的SPMD代码阐述任务并行的编译。

基本的串行2DFFT如下所示:

  REAL, DIMENSION(n,n) :: a1, a2

  DO WHILE(.true.)
   READ (unit = 1, end = 100) a1
   CALL rowffts(a1)
   a2 = a1
   CALL colffts(a2)
   WRITE (unit = 2) a2
   CYCLE  
100  CONTINUE
   EXIT
  ENDDO

为在HPF中写一个流水任务和数据并行2D FFT,需要对代码进行略微修改并增加几个HPF指令。首先,将变量a1和a2分配到不相连的处理器子集上,然后使用一个任务区域创建两个词法任务以便在不同的处理器子集上做rowffts和colffts。任务区域中的赋值a2=a1指明了任务间的数据转移。引入新变量done1来存储终止条件。修改后的代码如下所示:

     REAL DIMENSION(n,n) :: a1,a2
     LOGICAL done1
  !HPF$ PROCESSORS procs(8)

  !HPF$ DISTRIBUTE a1(block,*) ONTO procs(1:4)
  !HPF$ DISTRIBUTE a2(*,block) ONTO procs(5:8)

  !HPF$ TEMPLATE, DIMENSION(4), DISTRIBUTE(BLOCK) ONTO procs(1:4) :: td1
  !HPF$ ALIGN WITH td1(*) :: done1

  !HPF$ TASK_REGION
     done1 = .false.
     DO WHILE (.true.)
  !HPF$  ON HOME(procs(1:4)) BEGIN, RESIDENT
       READ (unit = iu,end=100) a1
       CALL rowffts(a1)
       GOTO 101
   100   done1 = .true.
   101   CONTINUE
  !HPF$  END ON

      IF (done1) EXIT
      a2 = a1

  !HPF$  ON HOME(procs(5:8)) BEGIN, RESIDENT
       CALL colffts(a2)
       WRITE(unit = ou) a2
  !HPF$  END ON
     ENDDO
  !HPF$ END TASK_REGION

最后,我们显示为每个处理器所生成的简化的SPMD代码。我们假定一个消息传递模型,在该模型中发送是异步的和非阻塞的,而接收直到数据到达之前都是阻塞的。我们使用一个简单的存储模型,在该模型中虽然一些变量仅在处理器子集上被引用,但是所有处理器上的变量定义都是相同的。由编译器创建一个阴影变量done1-copy来从处理器子集1向处理器子集2传送关于处理终止的信息。代码如下所示:

  REAL DIMENSION(n/4,n) :: a1
  REAL DIMENSION(n,n/4) :: a2
  LOGICAL done1

C  下面是编译器产生的变量
  LOGICAL done1_copy
  LOGICAL inset1, inset2

C

C  下面的编译器函数调用分别为处理器subset 1和subset 2将变量
C  inset1和inset2置为 .true.,否则置为.false.。
C
  CALL initialize_tasksets(inset1,inset2)

C  处理器子集 1的代码
  IF (inset1)
   done1 = .false.
   DO WHILE (.true.)

C  根据I/O模型读代码没有改变。
    READ (unit = 1,end=100) a1

    CALL rowffts(a1)
    GOTO 101
100   done1 = .true.
101   CONTINUE _send(done1,procs(5:8))
    IF (done1) EXIT
    _send(a1,proces(5:8))
   ENDDO
  ENDIF

C  处理器子集 2的代码
  IF (inset2)
   DO WHILE(.true.)
    _receive(done1_copy,procs(1:4))
    IF (local_done1) EXIT
    _receive(a2,procs(1:4))
    CALL colffts(a2)

C  根据I/O模型写代码没有改变
    WRITE (unit = 2) a2
   ENDDO
  ENDIF

_send和_receive是在处理器子集间传送变量的通信调用。程序一直执行到输入象下面这样结束。子集 1的处理器重复读输入,计算rowffts,并将计算输出和done1标志一起发送给子集 2的处理器,done1的值正常是.false.。子集 2的处理器接收标志和数据集合,计算colffts并将结果写到输出中。当输入结束时,子集 1的处理器将done1的值置为.true.,并把它发送出去,然后中止执行。子集 2的处理器接收标志,承认输入已结束并中止执行。


Copyright: NPACT BACKWARDFORWARD