本文简单介绍 Fluent UDF 串行代码并行化处理的问题。

注:以下内容来自 Fluent UDF 文档。

Fluent 求解器包含三种类型的执行器:Cortexhost 以及 node。当 Fluent 运行时,启动 1 个 Cortex 实例,之后启动 1 个 host 及 n 个 nodes,因此总共有有 n+2 个运行进程。因此,在并行计算(串行计算也推荐)时,用户需确保 UDF 成功地在 host 及 node 节点上运行。首先,可能需要用户准备两个不同的 UDF 版本:一个用于 host,另一个用于 node。不过最好的做法是编写一个 UDF,使其编译后可以在不同的版本上运行,这个过程称之为串行 UDF 的并行化处理。

用户可以向 UDF 中添加特殊的宏和编译器指令来实现这一过程。

编译器指令 (例如 #if RP_NODE、RP_HOST) 及其否定形式,能够指定编译器只编译 UDF 中应用于特定处理器的代码,而忽略其余部分,关于编译器指令的使用,我们在后面再详细描述。

如果串行 UDF 执行一个依赖于其他计算节点 (或主机) 发送或接收数据的操作,或者使用 Fluent 18.2 之后版本引入的宏类型,那么该串行 UDF 必须是能够并行的。还有一些操作必须将串行代码并行化:

  • 文件读写
  • Global Reductions
  • 全局求和
  • 全局求最小及最大值
  • 全局求逻辑值
  • 一些包含网格及网格面的循环
  • 在 console 窗口显示消息
  • 向 Host 或 Node 节点打印信息

当串行代码实现并行化修改后,即可采用与串行代码相同的方式进行编译及挂载。

DPM 模型可以使用以下下两种并行方式:

  • 共享内存(Shared Memory)
  • 消息传递(Message Passing)

当用户使用 DPM 相关的 UDF 宏时,其必须以上面该两种并行方式中的其中一种运行。由于 DPM 模型所需的所有流体变量保存在被跟踪的颗粒的数据结构中,因此在并行 Fluent 中使用 DPM UDF 时无需特别注意。不过以下两种情况例外:

  • 当使用 DEFINE_DPM_OUTPUT 宏输出颗粒信息时,不允许使用 c 函数 frpintf,此时应当使用特定的函数 par_fprintfpar_fprintf_head 采用并行方式写入文件。每个计算节点将颗粒信息写入到一个单独的临时文件,之后由 Fluent 排序后输出到最终文件。特定的输出函数与 c 函数 fprintf 采用相同的参数列表,但在当 Fluent 需要对文件进行排序时,需要指定一个扩展的参数列表。
  • 当存储颗粒信息时,并行模拟时需要使用特定的变量,这些变量可以通过宏 TP_USER_REAL(tp, i) (tp 是类型 Tracked_Particle ) 和 PP_USER_REAL(p, i) (p 是类型 Particle ) 访问。只有这些这样颗粒信息才能跨越分区边界,而其他局部或全局变量无法跨越分区边界。

需要注意,如果想要访问其他数据(如单元网格上的物理量),那么除了共享内存的并行方式外,用户可以访问所有流动和求解变量,但是如果采用了共享内存的方式,则只能访问宏 SV_DPM_LIST 和 SV_DPMS_LIST 中定义的变量。这些宏在 dpm.h 文件中定义。

本节包含可以用来并行化串行 UDF 的宏。可以在引用的头文件 (例如 para.h) 中找到这些宏的定义。

在将 UDF 转化为并行时,代码中的某些部分可能需要由 Host 节点完成,而另一部分则可能需要由 Node 节点完成。通过使用 Fluent 提供的编译器指令,可以分别指定代码哪些部分由 Host 或 Node 节点运行。用户为 Host 和 Node 节点编写一个 UDF 源文件,但是编译后可以生成不同的动态链接库版本。如用户可以将打印任务分配给 Host 节点,将任务计算整个区域网格的总体积分配给 node 节点。由于大多数操作系统是由 host 或 node 节点执行的,因此通常采用否定形式的编译器指令。

需要注意,Host 节点的主要目的是解释来自 Cortex 的命令或数据,并将命令或数据传递给 node-0 节点。由于 Host 节点中不包含网格数据,因此需要格外小心不要在任何计算中 Host 节点,以防出现分母为零的情况。在这种情况下,需要将这些操作包裹在#if !RP_HOST指令中,以指示编译器在执行与网格相关的计算时忽略 Host 节点。如想要利用 UDF 计算一个面 Thread 上的总面积,之后利用该总面积计算物理量的通量,如果不将 Host 排除在操作之外,则 Host 节点就是得到 的总面积为零,当 UDF 试图除以零计算通量时,将会出现浮点异常的错误提示。

代码示例:

#if !RP_HOST
    avg_pres = total_pres_a / total_area;
#endif

上面代码指定了求商操作在 node 节点上完成。

当需要从没有数据的操作中排除 node 节点时,可以使用 #if !RP_NODE 指令。

下面是一个并行编译器指令的列表,以及它们的作用

/*************************/
 /*  Compiler Directives */
/***********************/

 #if RP_HOST
    /* 只在Host节点中处理*/
#endif

 #if RP_NODE
    /* 只在Node节点中处理*/
#endif

 #if !RP_HOST
    /* 只在Node节点中处理*/
#endif

 #if !RP_NODE
    /*只在Host节点中处理*/
#endif

下面 UDF 简单展示了编译器指令的使用。DEFINE_ADJUST 宏中定义了一个名为 where_am_i 的函数。此函数查询以确定正在执行哪种类型的进程,然后在计算的节点上显示一条消息。

/*****************************************************
  Simple UDF that uses compiler directives
*****************************************************/
#include "udf.h"
DEFINE_ADJUST(where_am_i, domain)
{
#if RP_HOST
    Message("I am in the host process\n");
#endif /* RP_HOST */

 #if RP_NODE
    Message("I am in the node process with ID %d\n",myid);
#endif 
}

这种不同类型处理器之间的简单功能分配在实际情况下是有用的。例如,用户可能希望在运行特定计算时 (通过使用 RP_NODE 或! RP_HOST) 在计算节点上显示一条消息。或者用户也可以选择指定 host 进程来显示消息(通过使用 RP_HOST 或! RP_NODE)。通常,用户希望主机进程只写一次消息。或者用户可能希望从所有 nodes 收集数据,并从 host 打印一次总数。要执行这种类型的操作,UDF 需要在进程之间进行某种形式的通信。最常见的通信模式是 host 和 node 进程之间的通信。

Fluent 提供了两个宏用于 Host 与 Node 节点之间的数据通信:host_to_node_type_numnode_to_host_type_num

4.1 Host-to-Node 数据传递

从 Host 节点向所有 node 节点发送数据,可以使用宏 host_to_node_type_num。该宏的表达形式为:

host_to_node_type_num(val_1, val_2,...,val_num);

其中 num 是将在参数列表中传递的变量的数量,type 是将传递的变量的数据类型。可以传递的变量的最大数量是 7。数组和字符串也可以一次一个地从主机传递到节点,如下面的示例所示。

/* integer and real variables passed from host to nodes */
host_to_node_int_1(count);
host_to_node_real_7(len1, len2, width1, width2, breadth1, breadth2, vol);

 /* string and array variables passed from host to nodes */
char wall_name[]="wall-17";
int thread_ids[10] = {1,29,5,32,18,2,55,21,72,14};

 host_to_node_string(wall_name,8); /* remember terminating NUL character */
host_to_node_int(thread_ids,10);

注意,这些 host_to_node 通信宏不需要受并行 udf 的编译器指令保护,因为所有这些宏都会自动执行以下操作:

  • 如果编译为 Host 版本,则发送数据
  • 若编译为 node 版本,则接收数据

这组宏最常见的用途是将参数或边界条件从 Host 传递给 Node 节点。

4.2 Node-to-Host 数据传递

从 node-0 节点向 Host 节点发送数据,可以使用宏:

node_to_host_type_num(val_1,val_2,...,val_num);

其中 num 是将在参数列表中传递的变量的数量,type 是将传递的变量的数据类型。可以传递最多 7 个变量。如果想要传递更多的变量,可以使用数组。数组和字符串可以一次一个地从主机传递到节点,如下面的示例所示。

/* integer and real variables passed from compute node-0 to host */
node_to_host_int_1(count);
node_to_host_real_7(len1, len2, width1, width2, breadth1, breadth2, vol);

 /* string and array variables passed from compute node-0 to host */
char *string;
int string_length;
real vel[ND_ND];

 node_to_host_string(string,string_length);
node_to_host_real(vel,ND_ND);

host_to_node 宏是 host 节点将数据传递给所有的 node 节点(通过 node-0 节点间接传递),而 node_to_host 宏只是 node-0 节点向 host 节点传递数据。

node_to_host 宏不需要编译器指令 (例如 #if RP_NODE) 的保护,因为它们会自动执行以下操作

  • 如果节点是 node-0,并且将 UDF 编译为 node 版本,则发送数据
  • 如果 UDF 编译为 node 版本,但节点不是 - node0,则什么事情都不做
  • 如果 UDF 编译为 host 版本,则接收数据

这组宏最常见的用法是将 node-0 结果传递给 host。

在并行 Fluent 中有许多可以扩展为逻辑测试的宏。这些逻辑宏称为判断式,由后缀 P 表示,可以用作 UDF 中的测试条件。如果满足括号中的条件,以下判断式将返回 TRUE。

# define MULTIPLE_COMPUTE_NODE_P (compute_node_count > 1)
# define ONE_COMPUTE_NODE_P (compute_node_count == 1)
# define ZERO_COMPUTE_NODE_P (compute_node_count == 0)

有许多判断式允许使用计算节点 ID 来测试 UDF 中 node 节点标识。计算节点的 ID 存储为全局整数变量 myid。下面列出的每个宏都可以用来测试进程 myid 的某些条件。例如,判断式 I_AM_NODE_ZERO_P 将 myid 的值与 node-0 的 ID 进行比较,并在两者相同时返回 TRUE。另一方面,I_AM_NODE_SAME_P(n) 比较在 n 中传递的计算节点 ID 和 myid。当两个 id 相同时,函数返回 TRUE。节点 ID 判断式通常用于 udf 中的条件 - if 语句。

/* predicate definitions from para.h header file */

 # define I_AM_NODE_HOST_P (myid == host)
# define I_AM_NODE_ZERO_P (myid == node_zero)
# define I_AM_NODE_ONE_P (myid == node_one)
# define I_AM_NODE_LAST_P (myid == node_last)
# define I_AM_NODE_SAME_P(n) (myid == (n))
# define I_AM_NODE_LESS_P(n) (myid < (n))
# define I_AM_NODE_MORE_P(n) (myid > (n))

在一个分区网格中,一个面可能同时出现在一个或两个分区中,为了使求和操作不重复计算它,它只被正式分配给一个分区。上面的判断式与相邻网格的分区 ID 一起使用,以确定它是否属于当前分区。使用的约定是将编号较小的计算节点指定为该面的 principal 计算节点。如果面位于其 principal 计算节点上,则 PRINCIPAL_FACE_P 返回 TRUE。当希望对面执行全局且其中一些面是分区边界面时,可以使用该宏作为测试条件。下面是来自 para.h 的 PRINCIPAL_FACE_P 的定义。

/* predicate definitions from para.h header file */
# define PRINCIPAL_FACE_P(f,t) (!TWO_CELL_FACE_P(f,t) || \
  PRINCIPAL_TWO_CELL_FACE_P(f,t))

 # define PRINCIPAL_TWO_CELL_FACE_P(f,t) \
  (!(I_AM_NODE_MORE_P(C_PART(F_C0(f,t),THREAD_T0(t))) || \
  I_AM_NODE_MORE_P(C_PART(F_C1(f,t),THREAD_T1(t)))))

全局约简(global reduction)操作是从所有计算节点收集数据,并将数据约简为单个值或数组的操作。这些操作包括全局求和、全局最大值和最小值以及全局逻辑判断等。这些宏以前缀 PRF_G 开头,在头文件 prf.h 中定义。全局求和宏用后缀 SUM 表示、全局最大值用 HIGH 表示,全局最小值用 LOW 表示。后缀 AND 和 OR 标识全局逻辑。

变量数据类型在宏名称中标识,其中 R 表示实际数据类型,I 表示整数,L 表示逻辑。例如,宏 PRF_GISUM 查找计算节点上整数的总和。

每个全局约简宏都有两个不同的版本:一个采用单个变量参数,另一个采用变量数组。

  • 宏名称中带有后缀 1 的宏接受一个参数,并返回单个值作为全局约简结果。例如,宏 PRF_GIHIGH1(x) 接受一个参数 x 并在所有计算节点中计算变量 x 的最大值,然后返回该值。,如下面的示例所示。
{
     int y;
     int x = myid;
     y = PRF_GIHIGH1(x);
}
  • 没有 1 后缀的宏计算全局约简变量数组。这些宏有三个参数: x、N 和 iwork,其中 x 是一个数组,N 是数组中元素数量,iwork 是一个与临时存储所需的 x 类型和大小相同的数组。这种类型的宏被传递给一个数组 x,数组 x 的元素在从函数返回后被新的结果填充。例如,宏 PRF_GIHIGH(x,N,iwork) 计算 x 数组中每个元素在所有计算节点上的最大值,使用数组 iwork 作为临时存储,并通过将每个元素替换为结果的全局最大值来修改数组 x。该函数不返回值。
{
    real x[N], iwork[N];
    PRF_GRHIGH(x,N,iwork);
}

6.1 全局求和

可用于计算变量的全局和的宏由后缀 SUM 标识。宏 PRF_GISUM1 及 PRF_GISUM 分别计算整数变量和整数变量数组的全局和。

PRF_GRSUM1(x) 跨所有计算节点计算实变量 x 的和。运行单精度版本的 Fluent 时,全局和为浮点型,运行双精度版本时,全局和为双精度型。另外,PRF_GRSUM(x,N,iwork) 在双精度时返回浮点数组,单精度返回 double 数组。

宏形式宏描述
PRF_GISUM1(x)返回所有计算节点上整型变量 x 的和
PRF_GISUM(x,N,iwork)设置数组 x 存储所有计算节点上的和
PRF_GRSUM1(x)返回所有计算节点上的变量 x 的和,单精度返回 float,双精度返回 double
PRF_GRSUM(x,N,iwork)设置 x 为包含所有计算节点上变量的和的数组,单精度返回 float 数组,双精度返回 double 数组
注:数组调用为传址调用。

6.2 全局最大最小值

与全局求和类似,后缀为 HIGH 及 LO 的宏用于计算全局的最大值与最小值。

宏形式宏描述
PRF_GHIGH1(x)返回所有计算节点上整型变量 x 的最大值
PRF_GHIGH(x,N,iwork)设置 x 为包含所有计算节点上最大值的数组
PRF_GRHIGH1(x)返回所有计算节点上的变量 x 的最大值,单精度返回 float,双精度返回 double
PRF_GRHIGH(x,N,iwork)设置 x 为包含所有计算节点上变量最大值的数组,单精度返回 float 数组,双精度返回 double 数组
PRF_GILOW1(x)设置 x 为包含所有计算节点上最小值的数组
PRF_GILOW(x,N,iwork)设置 x 为包含所有计算节点上最小值的数组
PRF_GRLOW1(x)返回所有计算节点上的变量 x 的最小值,单精度返回 float,双精度返回 double
PRF_GRLOW(x,N,iwork)设置 x 为包含所有计算节点上变量最小值的数组,单精度返回 float 数组,双精度返回 double 数组

6.3 全局逻辑值

后缀 AND 及 OR 的宏可用于计算全局逻辑与及逻辑或的值。宏 PRF_GLOR1(x) 可以跨所有计算节点计算变量 x 的全局逻辑或。PRF_GLOR(x,N,iwork) 计算变量数组 x 的全局逻辑或。如果计算节点上的任何对应元素为 TRUE,则将 x 的元素设置为 TRUE。

宏形式宏描述
PRF_GLOR1(x)任何计算节点值为 TRUE 则返回 TRUE
PRF_GLOR1(x,N,work)任何元素为 TRUE 则返回 TRUE
PRF_GLAND1(x)所有计算节点 x 值为 TRUE 则返回 TRUE
PRF_GLAND(x)所有变量数组元素为 TRUE 则返回 TRUE

6.4 全局同步

如果希望在执行下一个操作之前全局同步计算节点,可以使用 PRF_GSYNC()。当在 UDF 中插入 PRF_GSYNC 宏时,在源代码中的上述命令在所有计算节点上完成之前,不会执行任何其他命令。在调试函数时,同步可能也很有用。

本文转载自:https://www.ershicimi.com/p/7248def2374aa2c0b042311607cb2980
最后修改:2020 年 09 月 10 日
如果觉得我的文章对你有用,请随意赞赏